Have Controller use new lifecycle events for filters.

--- src/controller/controller.coffee
+++ src/controller/controller.coffee
@@ -11,30 +11,19 @@
         Batman.developer.error("Please define `routingKey` on the prototype of #{Batman.functionName(@constructor)} in order for your controller to be minification safe.") if Batman.config.minificationErrors
         Batman.functionName(@constructor).replace(/Controller$/, '')

-  _optionsFromFilterArguments = (options, nameOrFunction) ->
-    if not nameOrFunction
-      nameOrFunction = options
-      options = {}
-    else
-      if typeof options is 'string'
-        options = {only: [options]}
-      else
-        options.only = [options.only] if options.only and Batman.typeOf(options.only) isnt 'Array'
-        options.except = [options.except] if options.except and Batman.typeOf(options.except) isnt 'Array'
-    options.block = nameOrFunction
-    options
+  @classMixin Batman.LifecycleEvents
+  @lifecycleEvent 'filter', (options = {}) ->
+    normalized = {}
+    only = if Batman.typeOf(options.only) is 'String' then [options.only] else options.only
+    except = if Batman.typeOf(options.except) is 'String' then [options.except] else options.except

-  @beforeFilter: ->
-    Batman.initializeObject this
-    options = _optionsFromFilterArguments(arguments...)
-    filters = @_batman.beforeFilters ||= []
-    filters.push(options)
+    normalized.if = (params) ->
+      return false if @_afterFilterRedirect
+      return false if only and params.action not in only
+      return false if except and params.action in except
+      return true

-  @afterFilter: ->
-    Batman.initializeObject this
-    options = _optionsFromFilterArguments(arguments...)
-    filters = @_batman.afterFilters ||= []
-    filters.push(options)
+    return normalized

   @afterFilter (params) ->
     if @autoScrollToHash && params['#']?
@@ -89,26 +78,28 @@
     @executeAction(action, params)

     redirectTo = @_afterFilterRedirect
+    @_afterFilterRedirect = null
     delete @_afterFilterRedirect

     Batman.redirect(redirectTo) if redirectTo

   executeAction: (action, params = @get('params')) ->
-    Batman.developer.assert @[action], "Error! Controller action #{@get 'routingKey'}.#{action} couldn't be found!"
+    Batman.developer.assert @[action], "Error! Controller action #{@get('routingKey')}.#{action} couldn't be found!"

     parentFrame = @_actionFrames[@_actionFrames.length - 1]
     frame = new Batman.ControllerActionFrame {parentFrame, action}, =>
-      @_runFilters action, params, 'afterFilters' unless @_afterFilterRedirect
+      @fireLifecycleEvent('afterFilter', params) if not @_afterFilterRedirect
       @_resetActionFrames()
       Batman.navigator?.redirect = oldRedirect

-    @_actionFrames.push frame
-    frame.startOperation({internal: true})
+    @_actionFrames.push(frame)
+    frame.startOperation(internal: true)

     oldRedirect = Batman.navigator?.redirect
     Batman.navigator?.redirect = @redirect
-    @_runFilters action, params, 'beforeFilters'
-    result = @[action](params) unless @_afterFilterRedirect
+
+    @fireLifecycleEvent('beforeFilter', params)
+    result = @[action](params) if not @_afterFilterRedirect

     @render() if not frame.operationOccurred
     frame.finishOperation()
@@ -131,7 +122,7 @@
         @_afterFilterRedirect = url
     else
       if Batman.typeOf(url) is 'Object'
-        url.controller = @ if not url.controller
+        url.controller = this if not url.controller

       Batman.redirect url

@@ -176,13 +167,3 @@
   _viewClassForAction: (action) ->
     classPrefix = @get('routingKey').replace('/', '_')
     Batman.currentApp?[Batman.helpers.camelize("#{classPrefix}_#{action}_view")] || Batman.View
-
-  _runFilters: (action, params, filters) ->
-    if filters = @constructor._batman?.get(filters)
-      for options in filters
-        continue if options.only and action not in options.only
-        continue if options.except and action in options.except
-        return if @_afterFilterRedirect
-
-        block = options.block
-        if typeof block is 'function' then block.call(@, params) else @[block]?(params)


Components: ConnectedManagedMachineWidget: Workaround for onClose issue of portal

--- client/app/lib/components/sidebarmachineslistitem/connectedmanagedmachinewidget.coffee
+++ client/app/lib/components/sidebarmachineslistitem/connectedmanagedmachinewidget.coffee
@@ -30,7 +30,8 @@

   removeFromStore: ->

-    actions.hideManagedMachineAddedModal @props.machine.get '_id'
+    kd.utils.defer =>
+      actions.hideManagedMachineAddedModal @props.machine.get '_id'

   handleButtonClick: ->


Improve spec coverage

Improve spec coverage

--- spec/body-row-spec.coffee
+++ spec/body-row-spec.coffee
@@ -25,6 +25,5 @@
     expect(showView().$el).toHaveClass 'body-row'

   it 'defaults the childView to a StringCell', ->
-    showView().children.each (stringCellView) ->
-      value = stringCellView.model.get(stringCellView.column.get('name'))
-      expect(stringCellView.$el).toHaveText value
+    renderedText = showView().children.map (stringCellView) -> stringCellView.$el.text()
+    expect(renderedText).toEqual ['hola', 'todos']


changed the test to make sure we have more than 3 neighbors

--- spec/lib/grid-spec.coffee
+++ spec/lib/grid-spec.coffee
@@ -44,6 +44,7 @@

   describe 'with any live cell with with more than three live neighbors dies', ->

+    Given -> @grid.live 3,4
     Given -> @grid.live 4,4
     Given -> @grid.live 4,3
     Given -> @grid.live 5,4


Tweak a456de97daaf1361bc3ad7df775f1b6cbe1f720e.

--- pages/options.coffee
+++ pages/options.coffee
@@ -32,7 +32,7 @@
   # Write this option's new value back to localStorage, if necessary.
   save: ->
     value = @readValueFromElement()
-    if JSON.stringify value != JSON.stringify @previous
+    if JSON.stringify(value) != JSON.stringify @previous
       bgSettings.set @field, @previous = value

   restoreToDefault: ->


Some additional work on posts.

Some additional work on posts.

--- public/javascripts/posts.coffee
+++ public/javascripts/posts.coffee
@@ -5,8 +5,7 @@

 $(document).ready ->
   $commits = $('section.commit')
-  $commits.each ->
-    commitOffsets.push [this.offsetTop, this]
+  $commits.each -> commitOffsets.push [this.offsetTop, this]

   bodyHeight = document.body.offsetHeight

@@ -20,42 +19,112 @@

 $(window).resize adjustMessageHeight

-lastScrollTop = 0
-currentConsiderationIndex = 0
 signOf = (num) -> if num < 0 then -1 else 1
 compareBySign = (sign, comparator, start, end) ->
   if sign < 0
     start < comparator
   else
     comparator < end
+
+# Runs a scroll to the next element. If scrollChange would scroll past
+# the next element, returns the remaining scrollChange. Otherwise, returns
+# 0.
+scrollToNextElement = (scrollChange, lastIndex, lastTop) ->
+  sign = signOf scrollChange
+
+  index = lastIndex + sign
+  [offsetTop, element] = commitOffsets[index]
+  diffChild = $(element).children('.diffs')[0]
+
+  if diffChild?
+    [scrollChange, lastTop] = scrollNext diffChild, scrollChange, lastTop
+
+  console.log "Post internal: #{scrollChange}"
+  return [scrollChange, lastIndex, lastTop, 0] if scrollChange <= 0
+
+  actualScroll =
+    if sign is 1
+      remainingScroll = scrollChange - (element.scrollHeight - element.offsetHeight)
+      console.log "Externally remaining: #{remainingScroll} vs #{scrollChange}"
+      Math.min Math.abs(scrollChange), remainingScroll
+    else
+      currentScroll = element.scrollTop
+      Math.min Math.abs(scrollChange), currentScroll
+
+  if (sign is 1 and remainingScroll > scrollChange) ||
+     element.scrollTop > Math.abs(scrollChange)
+    index = lastIndex
+  [scrollChange - actualScroll, index, lastTop + sign * actualScroll, actualScroll]
+
+scrollNext = (element, scrollChange, lastTop) ->
+  sign = signOf scrollChange
+
+  actualScroll =
+    if sign is 1
+      remainingScroll = element.scrollHeight - element.offsetHeight - element.scrollTop
+      console.log "Remaining: #{remainingScroll} vs #{scrollChange}"
+      Math.min Math.abs(scrollChange), remainingScroll
+    else
+      currentScroll = element.scrollTop
+      Math.min Math.abs(scrollChange), currentScroll
+
+  element.scrollTop += sign * actualScroll
+  console.log "Internal: #{sign * actualScroll}, #{element.scrollTop}", element
+  [scrollChange - actualScroll, lastTop + sign * actualScroll]
+
+currentIndex = -1
+lastScrollTop = 0
 $(document).scroll (event) ->
-  scrollTop = document.scrollTop || document.body.scrollTop
-  scrollChange = scrollTop - lastScrollTop
+  scrollTop = originalTop = document.scrollTop || document.body.scrollTop
+  currentScrollChange = scrollChange = scrollTop - lastScrollTop
+
+  console.log "Total change: #{scrollChange} @ #{currentIndex}."
+  overallChange = 0
+  while currentScrollChange > 0
+    [currentScrollChange, currentIndex, scrollTop, actualScroll] =
+      scrollToNextElement currentScrollChange, currentIndex, scrollTop
+
+    console.log "Result: #{actualScroll}; at #{currentScrollChange}"
+
+    overallChange += actualScroll
+
   lastScrollTop = scrollTop
+  console.log "In the end, #{overallChange}"

-  commit = commitOffsets[currentConsiderationIndex][1]
-  nextConsiderationIndex = currentConsiderationIndex + signOf(scrollChange)
-  nextOffsetParts = commitOffsets[nextConsiderationIndex]
+  $('h1, section.commit').each ->
+    $this = $(this)
+    top = parseInt $this.css('top')
+    $this.css 'top': "#{top - overallChange}px"

-  if nextOffsetParts?
-    [nextOffset, nextCommit] = nextOffsetParts

-  scrollingElement = undefined
-  diffs = $(commit).children('.diffs')
-  nextDiffs = $(nextCommit).children('.diffs')
-  if parseInt($(commit).css('top')) * signOf(scrollChange) <= 0 &&
-     compareBySign(signOf(scrollChange), diffs[0].scrollTop, 0, diffs[0].scrollHeight - diffs[0].offsetHeight)
-    scrollingElement = diffs[0]
-  else if parseInt($(nextCommit).css('top')) * signOf(scrollChange) <= 0
-    if compareBySign(signOf(scrollChange), nextDiffs[0].scrollTop, 0, nextDiffs[0].scrollHeight - nextDiffs[0].offsetHeight)
-      scrollingElement = nextDiffs[0]
-    else
-      currentConsiderationIndex = nextConsiderationIndex

-  if scrollingElement?
-    scrollingElement.scrollTop += scrollChange
-  else
-    $('h1, section.commit').each ->
-      $this = $(this)
-      top = parseInt $this.css('top')
-      $this.css 'top': "#{top - scrollChange}px"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


Don't send empty splat to console.log.

Don't send empty splat to console.log.

--- lib/docxconv/msg.coffee
+++ lib/docxconv/msg.coffee
@@ -14,4 +14,7 @@
     when 'info' then color = msg.info
     when 'done' then color = msg.done

-  return console.log color(message), args
+  if args.length > 0
+    return console.log color(message), args
+  else
+    return console.log color(message)


add empty search functionality

add empty search functionality

still bugged, because the search will lose focus and start to animate

--- app/assets/javascripts/app/controllers/navigation.js.coffee
+++ app/assets/javascripts/app/controllers/navigation.js.coffee
@@ -1,5 +1,8 @@
 class App.Navigation extends App.Controller
   className: 'navigation flex vertical'
+
+  events:
+    'click .empty-search': 'emptySearch'

   constructor: ->
     super
@@ -205,12 +208,18 @@
     @el.find('#global-search').bind( 'keyup', (e) =>
       @term = @el.find('#global-search').val()

+      @el.find('.search').toggleClass('filled', !!@term)
+
       return if !@term
       return if @term is search
       @delay( searchFunction, 220, 'search' )
     )

     @taskbar = new App.TaskbarWidget( el: @el.find('.tasks') )
+
+  emptySearch: (event) =>
+    @el.find('#global-search').val("")
+    @el.find('.search').removeClass('filled')

   getItems: (data) ->
     navbar =  _.values(data.navbar)


generalize enrollment conclusion confirmation message

closes #ADMIN-351

Change-Id: I7a8fcc33f13179dd3117165fb73f1b4aa10f917a
Reviewed-on: https://gerrit.instructure.com/131948
Tested-by: Jenkins
Reviewed-by: Jeremy Stanley <b3f594e10a9edcf5413cf1190121d45078c62290@instructure.com>
Product-Review: James Williams  <3a5f4ed991dbfa5c9ef76ad729d240ca12f62180@instructure.com>
QA-Review: James Williams  <3a5f4ed991dbfa5c9ef76ad729d240ca12f62180@instructure.com>

--- app/coffeescripts/bundles/legacy/context_roster_user.coffee
+++ app/coffeescripts/bundles/legacy/context_roster_user.coffee
@@ -43,7 +43,7 @@
     $(".conclude_enrollment_link").click (event) ->
       event.preventDefault()
       $(this).parents(".enrollment").confirmDelete
-        message: I18n.t("confirm.conclude_student", "Are you sure you want to conclude this student's enrollment?")
+        message: I18n.t("confirm.conclude", "Are you sure you want to conclude this enrollment?")
         url: $(this).attr("href")
         success: (data) ->
           comp_at = $.datetimeString(data.enrollment.completed_at)


changed shortcuts for demo mode next/prev

--- client/ide.coffee
+++ client/ide.coffee
@@ -224,8 +224,8 @@
             {'': 'New _Session', key: 'Ctrl+N', action: D.rideNewSession}
             '-'
             {'': '_Load Demo...', action: loadDemo}
-            {'': '_Next Line from Demo',     dontBindKey: 1, key: 'Alt+N', action: nextLineFromDemo}
-            {'': '_Previous Line from Demo', dontBindKey: 1, key: 'Alt+P', action: prevLineFromDemo}
+            {'': '_Next Line from Demo',     key: 'Ctrl+Shift+N', action: nextLineFromDemo}
+            {'': '_Previous Line from Demo', key: 'Ctrl+Shift+P', action: prevLineFromDemo}
           ]
             .concat(
               if D.process?.platform != 'darwin' then [ # Mac's menu already has an item for Quit


hide splitters for editor/tracer when RIDE starts

--- client/ide.coffee
+++ client/ide.coffee
@@ -151,6 +151,7 @@
         if !state.east. isClosed && (x = state.east .innerWidth ) > 1 then prefs.editorWidth  x
         if !state.south.isClosed && (x = state.south.innerHeight) > 1 then prefs.tracerHeight x
         return
+    for d in ['east', 'south'] then @layout.close d
     if !prefs.lbar() then @layout.hide 'north'
     @wins[0].updateSize()


Remove logs

Remove logs

--- spec/integration/changes-spec.coffee
+++ spec/integration/changes-spec.coffee
@@ -14,10 +14,9 @@
       atom.packages.activatePackage('git')

   afterEach ->
-    console.log "after"
     Reset = Git.Reset
     repository = null
-    console.log "projectPath", projectPath
+
     Git.Repository.open(projectPath).then (repo) ->
       repository = repo
       repo.head()


add comments; add some TODOs

add comments; add some TODOs

--- code/linode.coffee
+++ code/linode.coffee
@@ -6,6 +6,7 @@
 _s           = require('underscore.string')
 async        = require 'async'

+mex           = (require 'utility').mex
 LithiumConfig = (require 'lithium_config').LithiumConfig
 Instance      = (require 'instance').Instance

@@ -19,17 +20,23 @@
   @create: (config, callback) ->
     super config

+    # Create Linode via this waterfall.
+    # Parameters from the last function's call back are
+    # sent to the next function in the array.
     async.waterfall [
-      @_get_config_linode_plan,
-      @_linode_create,
-      @_update_linode_label,
-      @_have_updated,
-      @_create_disk,
-      @_disk_created,
-      @_create_linode_config,
+      @_get_config_linode_plan
+      @_linode_create
+      @_update_linode_label
+      @_have_updated
+      @_create_disk
+      @_disk_created
+      @_create_linode_config
     ], (err, result) =>
+      # This is called at the end of the waterfall,
+      # or if any of the callbacks returns an error.
+      # TODO: improve error handling
       console.log err if err?
-      @_got_linode_id result, callback
+      @_got_linode_id result, callback if result?

   @destroy: (name, callback) ->
     @get name, (instance) =>
@@ -49,6 +56,7 @@
       , (error, results) -> callback err, results

   # Returns an instance given its name.
+  # TODO: put err in to config
   @get: (name, callback) ->
     @list (err, list) ->
       k = _.find list, (n) ->
@@ -74,17 +82,15 @@
       , (err, res) ->
         callback()

-  ###

-  Only Private Methods Below Here
+  ####### Only Private Methods Below Here #######

-  ###
+  ### @create waterfall functions ###

+  # Get a config's corresponding Linode plan
   @_get_config_linode_plan: (callback) =>
     @_get_plan @config.ram, @config.disk_size, callback

-  # Private method to create a linode from a plan.  Called from
-  # @create; this does most of the actual work.
   @_linode_create: (plan_id, callback) =>
     @client.call 'linode.create',
       'DatacenterID': 7
@@ -107,13 +113,16 @@
   @_have_updated: (res, callback) =>
     @_get_distro @config.distribution, callback

+  # Create a system (distribution) disk
+  # TODO: multiple disks?
+  # TODO: change root password default
   @_create_disk: (id, callback) =>
     @client.call 'linode.disk.createfromdistribution',
       'LinodeID': @linode_id
       'DistributionID': id
       'Label': 'system'
-      'Size': @config.disk_size * 1000 # magic for now
-      'rootPass': 'r00ter' # magic for now
+      'Size': @config.disk_size * 1000
+      'rootPass': 'r00ter'
       'rootSSHKey': LithiumConfig.sshkey_public()
       , callback

@@ -134,6 +143,8 @@
   @_got_linode_id: (res, callback) =>
       @client.call 'linode.list', {LinodeID:@linode_id}, (err, res) ->
         callback err, _convert_to_instance res[0]
+
+  ### End create waterfall functions ###

   # Takes RAM as MB and disk as GB.
   @_get_plan: (ram, disk, callback) ->
@@ -169,12 +180,3 @@
   # bit at the end.
   config = o.name.replace /_\d+$/, ''
   new Linode config, o.id, o.name, o.state
-
-# :todo: move somewhere else
-# :todo: (exercise for reader) rewrite with O(n) complexity
-# Minimal EXcluded.
-# Given a set of numbers (non-negative integers), return the Minimal Excluded
-# number, which is the smallest number that is not in the set.
-# For example, mex([0,1,5]) == 2
-mex = (l) ->
-  _.min(i for i in [0..l.length] when i not in l)


Fixed an issue where graphs were blowing up when given an empty set of values in a layer.

--- coffee/time.coffee
+++ coffee/time.coffee
@@ -130,10 +130,13 @@
     @_ticks = []
     @_tickTimer = @options.windowSize % tickInterval

-    i = tickInterval
-    while i < @options.windowSize
-      @_pushTick(i, @data[0].values[i].time)
-      i += tickInterval
+    for layer in @data
+      continue unless layer.values? and layer.values.length > 0
+      i = tickInterval
+      while i < @options.windowSize and i < layer.values.length
+        @_pushTick(i, layer.values[i].time)
+        i += tickInterval
+      break

   # Builds and prepares the range axes (left and right)
   _prepareRangeAxes: ->


Fixed routes; removed contact us route for now

--- config/routes.coffee
+++ config/routes.coffee
@@ -20,11 +20,7 @@
     'what-is-new':
         title: 'What Is New'
         actions:
-            blog:
-                title: 'Blog'
-            events:
+            'events':
                 title: 'Events'
-    'contact-us':
-        title: 'Contact Us'

 module.exports = routes


update content security policy settings

update content security policy settings

--- config/http.coffee
+++ config/http.coffee
@@ -7,11 +7,20 @@
 			static: express.static('platforms/browser/www')
 			csp: (req, res, next)->
 				host = req.headers['x-forwarded-host'] || req.headers['host']
+				src = [
+					"'self'"
+					"filesystem:"
+					"data:"
+					"http://#{host}"
+					"https://#{host}"
+					"https://mob.myvnc.com"
+				]
 				ret = csp
 					directives:
-						connectSrc: [ "'self'", "ws://#{host}", "wss://#{host}" ]
-						styleSrc: [ "'self'", "'unsafe-inline'" ]
-						scriptSrc: [ "'self'", "'unsafe-inline'", "'unsafe-eval'" ]
+						defaultSrc: src
+						connectSrc: [ "ws://#{host}", "wss://#{host}" ].concat src
+						styleSrc: [ "'unsafe-inline'" ].concat src
+						scriptSrc: [ "'unsafe-inline'", "'unsafe-eval'" ].concat src
 				ret req, res, next
 			order: [
 				'cookieParser'


dispatch helper

dispatch helper

--- src/sky-helpers/index.coffee
+++ src/sky-helpers/index.coffee
@@ -1,7 +1,23 @@
 # These helpers make it easier to do HTTP and AWS things with your project's context.
+#
+
+# Provides the dispatching logic so Sky apps don't need to know how we
+# structure things.
+dispatch = (handlers) ->
+  (request, context, callback) ->
+    handler = handlers[context.functionName]
+    unless typeof handler is 'function'
+      console.error "Failed to execute: " + context.functionName
+      return callback new response.Internal()
+
+    handler request, context
+    .then (result) -> callback null, result
+    .catch (e) -> callback e
+

 module.exports = (AWS) ->
   async: require("fairmont").async
   response: require("./responses")
   s3: require("./s3")(AWS)
   method: require "./method"
+  dispatch: dispatch


don't show TimeTravel button for public documents

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -2045,7 +2045,7 @@
         @uncommitted_element = @element.find(".smc-uncommitted")

     init_history_button: () =>
-        if redux.getStore('account').get_editor_settings().track_revisions and @filename.slice(@filename.length-13) != '.sage-history'
+        if not @opts.public_access and @filename.slice(@filename.length-13) != '.sage-history'
             @history_button = @element.find(".salvus-editor-history-button")
             @history_button.click(@click_history_button)
             @history_button.show()


Fix rare file use bug

Fix rare file use bug

--- src/smc-webapp/file_use.cjsx
+++ src/smc-webapp/file_use.cjsx
@@ -500,7 +500,7 @@
         </Button>

     open_selected: ->
-        open_file_use_entry(@_visible_list?[@state.cursor].toJS(), @props.redux)
+        open_file_use_entry(@_visible_list?[@state.cursor]?.toJS(), @props.redux)

     render_list: ->
         v = @props.file_use_list.toArray()


webapp/new project quota ui: reset quotas button and using "componentWillReceiveProps" to update state before rendering

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -645,6 +645,13 @@
             description_text : ''
             error            : ''

+    componentWillReceiveProps : (nextProps) ->
+        # https://facebook.github.io/react/docs/component-specs.html#updating-componentwillreceiveprops
+        @setState(@getInitialUpgraderState())
+        subs = @props.customer?.subscriptions?.total_count ? 0
+        if subs > 0 and not @state["has_subbed"]
+            @setState(has_subbed: true)
+
     get_quota_limits : ->
         # NOTE : all units are currently 'internal' instead of display, e.g. seconds instead of hours

@@ -694,6 +701,9 @@

     max_upgrades : ->
         @setState(@getUpgraderState(true))
+
+    reset_upgrades : ->
+        @setState(@getUpgraderState(false))

     show_upgrade_quotas : ->
         @setState(upgrading : true)
@@ -855,15 +865,6 @@

<Icon name='arrow-circle-up' /> Adjust your project quota contributions

                 Adjust your contributions to the quotas on this project (disk space, memory, cores, etc.).  The total quotas for this project are the sum of the contributions of all collaborators and the free base quotas.
-

-                    <Button
-                        bsSize  = 'xsmall'
-                        onClick = {=>@max_upgrades()}
-                        style   = {padding:'0px 5px'}
-                    >
-                        Max all the upgrades
-                    </Button>
-

                 <Row>
                     <Col md=6>
@@ -871,6 +872,22 @@
                     </Col>
                     <Col md=6>
                         Your contribution
+

+                        <Button
+                            bsSize  = 'xsmall'
+                            onClick = {=>@max_upgrades()}
+                            style   = {padding:'0px 5px'}
+                        >
+                            Max all upgrades
+                        </Button>
+                        {' '}
+                        <Button
+                            bsSize  = 'xsmall'
+                            onClick = {=>@reset_upgrades()}
+                            style   = {padding:'0px 5px'}
+                        >
+                            Reset all upgrades
+                        </Button>
                     </Col>
                 </Row>

@@ -968,19 +985,18 @@
         $('html, body').animate({ scrollTop: $('#upgrade_before_creation').offset().top }, 0)

     render_upgrade_before_create : (subs) ->
-        if subs > 0 and not @state["has_subbed"]
-            @setState(@getInitialUpgraderState())
-            @setState(has_subbed: true)
         <Col sm=12>

Upgrade to give your project internet access and more resources

-
To prevent abuse the free version doesn't have internet access.
-            Installing software from the internet, using Github/Bitbucket/Gitlab/etc, and/or
-            any other internet resources
-            is not possible with the free version.
-            Starting at just $7/month you can give your project(s)
-            internet access, members only hosting, 1 day Idle timeout,
-            3 GB Memory, 5 GB Disk space, and half CPU share. You can share upgrades
-            with any project you are a collaborator on.

+

+                To prevent abuse the free version doesn{"'"}t have internet access.
+                Installing software from the internet, using Github/Bitbucket/Gitlab/etc, and/or
+                any other internet resources
+                is not possible with the free version.
+                Starting at just $7/month you can give your project(s)
+                internet access, members only hosting, 1 day Idle timeout,
+                3 GB Memory, 5 GB Disk space, and half CPU share. You can share upgrades
+                with any project you are a collaborator on.
+

                 {

 if subs == 0}
                 <BillingPageSimplifiedRedux redux={redux} />


Fix: could not generate the UI in dependant projects

--- brunch-config.coffee
+++ brunch-config.coffee
@@ -3,7 +3,7 @@
   files:
     javascripts:
       joinTo:
-        'libraries.js': /^(bower_components|node_modules|vendor)[\\/]/
+        'libraries.js': /(bower_components|node_modules|vendor)[\\/]/
         'app.js': /^app/
       order:
         before: [/js\/controllers\//]


:bug: add case for matching beginning of path

--- lib/fuzzy-finder-view.coffee
+++ lib/fuzzy-finder-view.coffee
@@ -55,6 +55,8 @@
           matchedChars.push(path[matchIndex])
           lastIndex = matchIndex + 1

+        @span matchedChars.join(''), class: 'matching' if matchedChars.length
+
         # Remaining characters are plain text
         @text path.substring(lastIndex)


change strong to span and change class name

--- lib/fuzzy-finder-view.coffee
+++ lib/fuzzy-finder-view.coffee
@@ -50,7 +50,7 @@
           matchedChar = path[matchIndex]

           @text unmatched if unmatched
-          @strong matchedChar, class: 'matched-char'
+          @span matchedChar, class: 'matching'
           lastIndex = matchIndex + 1

         # Remaining characters are plain text


Call populateProjectPaths when callback completes

This removes duplicate code for the cases when a callback
is used and when the view already has populated paths.

--- lib/fuzzy-finder-view.coffee
+++ lib/fuzzy-finder-view.coffee
@@ -157,15 +157,7 @@
       callback = (paths) =>
         @projectPaths = paths
         @reloadProjectPaths = false
-        listedItems =
-          if options.filter?
-            @projectPaths.filter (path) ->
-              path.indexOf(options.filter) >= 0
-          else
-            @projectPaths
-
-        @setArray(listedItems)
-        options.done(listedItems) if options.done?
+        @populateProjectPaths(options)
       @loadPathsTask = new LoadPathsTask(callback)
       @loadPathsTask.start()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -70,6 +70,7 @@

   return target

+String::constantize
 return this.match(/\.([^\.]*)$/, '').last() || '';

 ###*


Added count for invites sent

Added count for invites sent

--- client/admin/lib/views/invitations/invitesomeoneview.coffee
+++ client/admin/lib/views/invitations/invitesomeoneview.coffee
@@ -97,7 +97,10 @@
       invites.push invite = view.serialize()
       admins.push invite.email  if invite.role is 'admin'

-    Tracker.track Tracker.INVITED_TEAMMEMBERS
+    Tracker.track Tracker.INVITED_TEAMMEMBERS, {
+      invites_count : invites.length
+      admins_count  : admins.length
+    }

     if admins.length
       @notifyAdminInvites invites, admins


removed unnecessary currentSuggestionIndex

--- lib/atom-ternjs-autocomplete.coffee
+++ lib/atom-ternjs-autocomplete.coffee
@@ -46,7 +46,6 @@

     preBuildSuggestions: ->
         suggestionsArr = []
-        @currentSuggestionIndex = false
         @checkCompletion().then (data) =>
             return unless data?.length
             for obj, index in data
@@ -57,8 +56,8 @@
             @triggerCompletion()

     triggerCompletion: =>
+        @getCurrentAutocompleteManager().runAutocompletion()
         @currentSuggestionIndex = 0
-        @getCurrentAutocompleteManager().runAutocompletion()
         @setDocumentationContent()

     setDocumentationContent: ->


Twitter button was showing duplicated

Twitter button was showing duplicated

--- client/landing/app/MainApp/book/embedded/socialshare.coffee
+++ client/landing/app/MainApp/book/embedded/socialshare.coffee
@@ -18,7 +18,7 @@
       href          : "https://twitter.com/koding"
       partial       : ""

-    @twitter.on "viewAppended", =>
+    @twitter.once "viewAppended", =>
       protocol = if /^http:/.test document.location then 'http' else 'https'
       require ["#{protocol}://platform.twitter.com/widgets.js"], =>
         twttr.widgets.createFollowButton(


structure for message input img upload

structure for message input img upload

--- src/chat_room_view.coffee
+++ src/chat_room_view.coffee
@@ -46,11 +46,8 @@

-

-          <textarea class="field test" placeholder="Enter Message...">
-          </textarea>
-          <button class="item entypo-camera" id="bye"></button>
-

+        <textarea class="grw" placeholder="Enter Message..."></textarea>
+

     ')


Add simple hooks/event trigger points (closes #6)

--- commandant.coffee
+++ commandant.coffee
@@ -55,11 +55,19 @@
     else
       @stack_idx = 0
     @stack = []
+
+    @trigger?('reset', rollback)
+    @onReset?(rollback)
+
     return

   # Register a new named command to be available for execution.
   register: (name, command) ->
     @commands[name] = command
+
+    @trigger?('register_command', name, command)
+    @onRegisterCommand?(name, command)
+
     return

   # Execute a function without recording any command executions.
@@ -104,8 +112,12 @@

     @_assert(!@_transient, 'Cannot redo while transient action active.')

-    @_run(@stack[@stack_idx], 'run')
+    action = @stack[@stack_idx]
+    @_run(action, 'run')
     ++@stack_idx
+
+    @trigger?('redo', action)
+    @onRedo?(action)

     return

@@ -116,7 +128,11 @@
     @_assert(!@_transient, 'Cannot undo while transient action active.')

     --@stack_idx
-    @_run(@stack[@stack_idx], 'undo')
+    action = @stack[@stack_idx]
+    @_run(action, 'undo')
+
+    @trigger?('undo', action)
+    @onUndo?(action)

     return

@@ -170,6 +186,10 @@
     @stack.splice(@stack_idx, Infinity)
     @stack.push action
     @stack_idx = @stack.length
+
+    @trigger?("execute", action)
+    @onExecute?(action)
+
     return

   _assert: (val, message) ->


Remove spurious TODO

Remove spurious TODO

--- public/src/list.cjsx
+++ public/src/list.cjsx
@@ -15,7 +15,6 @@
     if @props.onClick
       classes += ' button'

-    # TODO: add links

       {@props.children}

{@props.title}


Disabling process title updating

Disabling process title updating

--- src/coffeeq_worker.coffee
+++ src/coffeeq_worker.coffee
@@ -123,7 +123,7 @@
   perform: (job) ->
     old_title = process.title
     @emit 'job', @, @queue, job
-    @procline "#{@queue} job since #{(new Date).toString()}"
+
     if cb = @callbacks[job.class]
       cb job.args..., (result) =>
         try
@@ -159,15 +159,7 @@
   recordFailure: (err, job) ->
     key = @errorKeyForQueue(@queue)
     @queueClient.rpush key, "Error processing:#{JSON.stringify(job)} | #{err} | #{Date()}", (err, val) =>
-      console.log "pushed error on queue #{@queue}"
-
-  ###
-  Sets the process title.
-  msg - The String message for the title.
-  Returns nothing.
-  ###
-  procline: (msg) ->
-    process.title = "coffeeq: #{msg}"
+      console.log "pushed error on queue #{@queue}"

   # extend object
   Object.defineProperty @prototype, 'name',


Fix deactivation of image toolbar button after deletion.

--- src/modules/image-tooltip.coffee
+++ src/modules/image-tooltip.coffee
@@ -30,6 +30,7 @@
     dom(@textbox).on('input', _.bind(this._preview, this))
     this.initTextbox(@textbox, this.insertImage, this.hide)
     @quill.onModuleLoad('toolbar', (toolbar) =>
+      @toolbar = toolbar
       toolbar.initFormat('image', _.bind(this._onToolbar, this))
     )

@@ -54,6 +55,7 @@
       )
     else
       @quill.deleteText(range, 'user')
+      @toolbar.setActive('image', false)

   _preview: ->
     return unless this._matchImageURL(@textbox.value)


Some refactoring in git-commit.coffee

Some refactoring in git-commit.coffee

--- lib/models/git-commit.coffee
+++ lib/models/git-commit.coffee
@@ -81,7 +81,7 @@

   splitPane: (oldEditor) ->
     pane = atom.workspace.paneForURI(@filePath())
-    splitDir = if atom.config.get('git-plus.openInPane') then atom.config.get('git-plus.splitPane') else 'right'
+    splitDir = if atom.config.get('git-plus.openInPane') then atom.config.get('git-plus.splitPane')
     options = { copyActiveItem: true }
     hookEvents = (textEditor) =>
       oldEditor.destroy()
@@ -116,14 +116,10 @@
         notifier.addSuccess data
         if @andPush
           new GitPush(@repo)
-        # Set @isAmending to false since it succeeded.
         @isAmending = false
-        # Destroying the active EditorView will trigger our cleanup method.
         @destroyActiveEditorView()
         # Activate the former active pane.
         @currentPane.activate() if @currentPane.alive
-        # Refreshing the atom repo status to refresh things like TreeView and diff gutter.
-        # Refresh git index to prevent bugs on our methods.
         git.refresh()

       stderr: (err) =>


Update google_map.coffee

Update google_map.coffee

--- app/assets/javascripts/geocoder_google_maps/google_map.coffee
+++ app/assets/javascripts/geocoder_google_maps/google_map.coffee
@@ -1,5 +1,5 @@
 $(document).ready ->
-  $('div.geocoder-google-map').each (index, mapElement) =>
+  $('div.geocoder-google-maps').each (index, mapElement) =>
     jQMapElement = $(mapElement)
     mapOptions =
       zoom: jQMapElement.data('zoom')


Changed variable name.

Changed variable name.

It actually is a media query to select widths under the sm cutoff, that is, widths that belong to the xs- responsive Bootstrap category.

--- app/assets/javascripts/ng-services/ng-AnimationsSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-AnimationsSvc.js.coffee
@@ -90,7 +90,7 @@
   #--------------------------------------------
   # Media query to hide sidebar only in smartphone screens
   #--------------------------------------------
-  sm_max_media_query = 'screen and (max-width: 768px)'
+  xs_max_media_query = 'screen and (max-width: 768px)'

   service =

@@ -98,14 +98,14 @@
     # Animate hiding the sidebar if the screens is smartphone-sized.
     #---------------------------------------------
     hide_sidebar: ->
-      enquire.register sm_max_media_query, ->
+      enquire.register xs_max_media_query, ->
         $('#sidebar-column').velocity {translateX: '-100%'}, {duration: 300, easing: 'swing'}

     #---------------------------------------------
     # Animate showing the sidebar if the screens is smartphone-sized.
     #---------------------------------------------
     show_sidebar: ->
-      enquire.register sm_max_media_query, ->
+      enquire.register xs_max_media_query, ->
         $('#sidebar-column').velocity {translateX: '0'}, {duration: 300, easing: 'swing'}

     #---------------------------------------------


Remove autoscrolling of the sidebar when opening a folder.

It's never worked reliably, velocity.js does not react well to scrolling it, probably because it's surrounded by a container.

--- app/assets/javascripts/ng-services/ng-AnimationsSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-AnimationsSvc.js.coffee
@@ -212,14 +212,11 @@
       height_auto = folder_content.outerHeight() + padding_top + padding_bottom

       # Set height back to 0px and animate the transition to its final height
-      # After finishing opening animation, scroll to show as much of the folder content as possible.
-      # We leave an offset so that part of the folder above is still visible under the navbar.
       topOffset = -100
       folder_content
       .css('height', '0')
       .velocity {height: height_auto, 'padding-top': padding_top, 'padding-bottom': padding_bottom},
         {duration: 300, easing: 'swing', complete: add_folder_open_class}
-      .velocity 'scroll', {container: sidebar_column, offset: topOffset, duration: 250, delay: 50}

       # Rotate folder arrow 90 degrees clockwise (pointing down)
       $("#open-folder-#{folder.id} .folder-arrow")


Prettier code in angularjs service.

Prettier code in angularjs service.

Of course beauty is in the eye of the beholder. So it may look uglier to you. Or the beholder may hit you with a disintegration beam from one of its eyestalks. Lesson here is, there are more important things in life to worry about.

--- app/assets/javascripts/ng-services/ng-FolderMgmtSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-FolderMgmtSvc.js.coffee
@@ -20,51 +20,55 @@
       else
         folder.unread_entries -= feed.unread_entries

-  #--------------------------------------------
-  # Remove a feed from a folder
-  #--------------------------------------------
-  remove_from_folder: ->
-    folder_id = currentFeedSvc.get().folder_id
-    currentFeedSvc.get().folder_id = 'none'
-    feed_removed currentFeedSvc.get(), folder_id
+  service =

-    $http.put('/folders/none.json', folder: {feed_id: currentFeedSvc.get().id})
-    .error ->
-      timerFlagSvc.start 'error_managing_folders'
+    #--------------------------------------------
+    # Remove a feed from a folder
+    #--------------------------------------------
+    remove_from_folder: ->
+      folder_id = currentFeedSvc.get().folder_id
+      currentFeedSvc.get().folder_id = 'none'
+      feed_removed currentFeedSvc.get(), folder_id

-  #--------------------------------------------
-  # Update the model to account for a feed having been removed from a folder
-  #--------------------------------------------
-  feed_removed_from_folder: feed_removed
+      $http.put('/folders/none.json', folder: {feed_id: currentFeedSvc.get().id})
+      .error ->
+        timerFlagSvc.start 'error_managing_folders'

-  #--------------------------------------------
-  # Move a feed to an already existing folder
-  #--------------------------------------------
-  move_to_folder: (folder)->
-    old_folder_id = currentFeedSvc.get().folder_id
-    currentFeedSvc.get().folder_id = folder.id
-    feed_removed currentFeedSvc.get(), old_folder_id
-    folder.unread_entries += currentFeedSvc.get().unread_entries
+    #--------------------------------------------
+    # Update the model to account for a feed having been removed from a folder
+    #--------------------------------------------
+    feed_removed_from_folder: feed_removed

-    $http.put("/folders/#{folder.id}.json", folder: {feed_id: currentFeedSvc.get().id})
-    .error ->
-      timerFlagSvc.start 'error_managing_folders'
+    #--------------------------------------------
+    # Move a feed to an already existing folder
+    #--------------------------------------------
+    move_to_folder: (folder)->
+      old_folder_id = currentFeedSvc.get().folder_id
+      currentFeedSvc.get().folder_id = folder.id
+      feed_removed currentFeedSvc.get(), old_folder_id
+      folder.unread_entries += currentFeedSvc.get().unread_entries

-  #--------------------------------------------
-  # Move a feed to a new folder
-  #--------------------------------------------
+      $http.put("/folders/#{folder.id}.json", folder: {feed_id: currentFeedSvc.get().id})
+      .error ->
+        timerFlagSvc.start 'error_managing_folders'

-  move_to_new_folder: (title)->
-    if title
-      $http.post("/folders.json", folder: {feed_id: currentFeedSvc.get().id, title: title})
-      .success (data)->
-        $rootScope.folders.push data
-        old_folder_id = currentFeedSvc.get().folder_id
-        currentFeedSvc.get().folder_id = data.id
-        feed_removed currentFeedSvc.get(), old_folder_id
-      .error (data, status)->
-        if status == 304
-          timerFlagSvc.start 'error_already_existing_folder'
-        else
-          timerFlagSvc.start 'error_creating_folder'
+    #--------------------------------------------
+    # Move a feed to a new folder
+    #--------------------------------------------
+
+    move_to_new_folder: (title)->
+      if title
+        $http.post("/folders.json", folder: {feed_id: currentFeedSvc.get().id, title: title})
+        .success (data)->
+          $rootScope.folders.push data
+          old_folder_id = currentFeedSvc.get().folder_id
+          currentFeedSvc.get().folder_id = data.id
+          feed_removed currentFeedSvc.get(), old_folder_id
+        .error (data, status)->
+          if status == 304
+            timerFlagSvc.start 'error_already_existing_folder'
+          else
+            timerFlagSvc.start 'error_creating_folder'
+
+  return service
 ]


Added client-side validation check before submitting

--- app/assets/javascripts/sprangular/controllers/checkout.coffee
+++ app/assets/javascripts/sprangular/controllers/checkout.coffee
@@ -6,6 +6,10 @@
   $scope.submit = ->
     $scope.processing = true

+    if $scope.order.isInvalid()
+      $scope.processing = false
+      return
+
     Checkout.update()
       .success ->
         $location.path('/checkout/confirm')


remove uploadcare.initialize from api build set right uploadcare.start for api build

--- app/assets/javascripts/uploadcare/build/uploadcare.api.coffee
+++ app/assets/javascripts/uploadcare/build/uploadcare.api.coffee
@@ -7,8 +7,7 @@
 {expose} = uploadcare

 expose('globals', uploadcare.settings.common)
-expose('start')
-expose('initialize')
+expose('start', uploadcare.settings.common)
 expose('fileFrom')
 expose('filesFrom')
 expose('FileGroup')


Fix a bug preventing logging of APNS feedback unsubscriptions

--- lib/pushservices/apns.coffee
+++ lib/pushservices/apns.coffee
@@ -19,7 +19,7 @@
             @logger?.debug("APNS feedback returned #{feedbackData.length} devices")
             feedbackData.forEach (item) =>
                 tokenResolver 'apns', item.device.toString(), (subscriber) =>
-                    subscriber?.get (info) ->
+                    subscriber?.get (info) =>
                         if info.updated < item.time
                             @logger?.warn("APNS Automatic unregistration for subscriber #{subscriber.id}")
                             subscriber.delete()


Workspace: Added terminal reference to launcher.

--- client/landing/app/CommonViews/workspace/views/workspacefloatingpanelauncher.coffee
+++ client/landing/app/CommonViews/workspace/views/workspacefloatingpanelauncher.coffee
@@ -64,13 +64,13 @@
       cssClass   : "floating-pane"
       size       : height : 400

-    @terminal.addSubView terminalPane = new terminalClass
+    @terminal.addSubView @terminalPane = new terminalClass
       delegate   : @panel
       sessionKey : @sessionKeys.terminal

-    terminalPane.webterm.on "WebTermConnected", =>
+    @terminalPane.webterm.on "WebTermConnected", =>
       @keysRef.child("terminal").set
-        key    : terminalPane.remote.session
+        key    : @terminalPane.remote.session
         host   : KD.nick()
         vmName : KD.getSingleton("vmController").defaultVmName


Added new properties

Added new properties

--- lib/snippets-provider.coffee
+++ lib/snippets-provider.coffee
@@ -34,6 +34,9 @@
         text: snippet.prefix
         replacementPrefix: prefix
         rightLabel: snippet.name
+        rightLabelHTML: snippet.rightLabelHTML
+        leftLabel: snippet.leftLabel
+        leftLabelHTML: snippet.leftLabelHTML
         description: snippet.description
         descriptionMoreURL: snippet.descriptionMoreURL


Added timeout for async tests

Added timeout for async tests

--- lib/testing/structure.coffee
+++ lib/testing/structure.coffee
@@ -3,6 +3,8 @@
 {utils} = Neft
 stack = require './stack'
 logger = require './logger'
+
+ASYNC_TEST_TIMEOUT = 5000

 class Scope
     constructor: ->
@@ -97,6 +99,9 @@
             stack.callFunction @testFunction, @context
             @onEnd()
         else
+            setTimeout =>
+                @onEnd new Error 'Test time out'
+            , ASYNC_TEST_TIMEOUT
             stack.callFunction @testFunction, @context, [@onEnd]

         return


only make network/internet banner an error after project used for a long time.

--- src/smc-webapp/project_page.cjsx
+++ src/smc-webapp/project_page.cjsx
@@ -227,12 +227,12 @@
             height     : 0
         ×

-    render_learn_more: ->
+    render_learn_more: (color) ->

-            learn more...
+            style  = {fontWeight : 'bold', color:color, cursor:'pointer'}>
+            <Space/> — learn more... <Space/>

         #@actions(project_id: @props.project_id).show_extra_free_warning()} style={color:'white', cursor:'pointer'}> learn more...

@@ -254,15 +254,17 @@
         if not host and not internet
             return null

-        font_size = Math.min(18, 12 + Math.round((@props.project_log?.size ? 0) / 30))
+        font_size = Math.min(18, 12 + Math.round((@props.project_log?.size ? 0) / 60))
         styles =
             padding      : 3
             paddingLeft  : 7
             paddingRight : 7
             marginBottom : 0
             fontSize     : "#{font_size}pt"
-            color        : 'white'
-            background   : 'red'
+
+        if font_size > 14 # only get obnoxious after a while...
+            styles.color      = 'white'
+            styles.background = 'red'

         ###
         if @props.free_compute_slowdown? and @props.free_compute_slowdown > 0.0
@@ -272,10 +274,10 @@
             slowdown = ''
         ###

-        <Alert bsStyle='danger' style={styles}>
+        <Alert bsStyle='warning' style={styles}>
             <Icon name='exclamation-triangle' style={float:'right', marginTop: '3px'}/>
-            <Icon name='exclamation-triangle' /> Upgrade this project, since it is {on a free server if host} {without internet access if internet}. —
-            {@render_learn_more()}
+            <Icon name='exclamation-triangle' /> Upgrade this project, since it is {on a free server if host} {without network access if internet}.
+            {@render_learn_more(styles.color)}
             {@render_dismiss()}
             {@extra(host, internet)}
         </Alert>


Sample font styling when font config values change

We previously could do it whenever stylesheets changed, but these values
end up getting assigned to the global stylesheet for cascading reasons
and we’re only watching the local stylesheet. We poll the host elements
DOM properties, but forcing a sync poll when the config values change
makes behavior synchronous for specs and more responsive when changing
these values.

--- src/text-editor-component.coffee
+++ src/text-editor-component.coffee
@@ -416,6 +416,9 @@

   observeConfig: ->
     @subscribe atom.config.observe 'editor.useHardwareAcceleration', @setUseHardwareAcceleration
+    @subscribe atom.config.onDidChange 'editor.fontSize', @sampleFontStyling
+    @subscribe atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling
+    @subscribe atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling

   onGrammarChanged: ->
     {editor} = @props


Added callbacks for actions

Added callbacks for actions

--- src/app/mixins/model-manager.coffee
+++ src/app/mixins/model-manager.coffee
@@ -15,15 +15,23 @@
     Ember.Object.create title: '取消', dismiss: 'modal'
   ]

+  beforeSave: Ember.K
+  afterSave: Ember.K
+
+  # TODO 删除该方法
   beforeConfirmRemove: Ember.K
+  beforeRemove: Ember.K
+  afterRemove: Ember.K

   actions:
     revertChanges: ->
       @get('model').rollback() if @get('model.isDirty')

     save: ->
+      return unless @beforeSave()
       @get('model').save().then =>
         Notifier.success("保存#{@get('humanModelName')}成功")
+        @afterSave()
         @transitionToRoute(@get('afterSaveRoute'), @get('model'))
       , => Notifier.error("保存#{@get('humanModelName')}失败")

@@ -37,10 +45,12 @@
     confirmRemove: ->
       Bootstrap.ModalManager.hide('removeConfirmation')

-      return false unless @beforeConfirmRemove()
+      # TODO Remove `@beforeConfirmRemove()`
+      return false unless @beforeConfirmRemove() && @beforeRemove()

       @get('model').deleteRecord()
       @get('model').save().then =>
         Notifier.success("删除#{@get('humanModelName')}成功")
+        @afterRemove()
         @transitionToRoute(@get('afterDestroyRoute'))
       , => Notifier.error("删除#{@get('humanModelName')}失败")


javascript typos

javascript typos

--- maproulette/static/js/maproulette.coffee
+++ maproulette/static/js/maproulette.coffee
@@ -149,11 +149,11 @@
   ###
   # Takes dialog box data and returns a dialog box for nextUp actions
   ###
-  dlg = $('

').addclass("dlg")
-  dlg.append(markdown.toHTML(dlgData.text))
-  buttons = $('div').addclass("buttons")
+  dlg = $('

').addClass("dlg")
+  dlg.append(markdown.makeHtml(dlgData.text))
+  buttons = $('div').addClass("buttons")
   for item in dlgData.buttons
-    button = $('div').addclass("button")
+    button = $('div').addClass("button")
     button.attr {onclick: "#{item.action}"}
     button.content(item.label)
     buttons.append(button)
@@ -253,7 +253,7 @@
   ###
   # Displays a task to the display and waits for the user prompt
   ###
-  drawFeaures(task.features)
+  drawFeatures(task.features)
   revGeocode()
   setDelay 3, msgClose()
   msgTaskText()
@@ -285,11 +285,11 @@
   # Gets another task from the current challenge, close to the
   # location (if supplied)
   ###
-  near = "#{map.getCenter().lat},#{map.getCenter().lon}" if not near
-  url = "/api/challenges/#{challenge.slug}/tasks?near=#{near}"
+  near = "#{map.getCenter().lat},#{map.getCenter().lng}" if not near
+  url = "/api/challenges/#{challenge}/task?near=#{near}"
   $.getJSON url, (data) ->
     currentTask = data
-    currentTask.startTime = new Date.getTime()
+    currentTask.startTime = new Date().getTime()
     showTask(data)

 changeMapLayer = (layerUrl, layerAttrib = tileAttrib) ->
@@ -401,23 +401,23 @@
   # Get the stats for the challenge and display the count of remaining
   # tasks
   ###
-  $.getJSON "c/#{challenge}/stats", (data) ->
-    remaining = data.total - data.done
+  $.getJSON "/api/challenges/#{challenge}/stats", (data) ->
+    remaining = data.stats.total - data.stats.done
     $("#counter").text remaining

 updateChallenge = (challenge) ->
   ###
   # Use the current challenge metadata to fill in the web page
   ###
-  $.getJSON "/c/#{challenge}/meta", (data) ->
-    currentChallenge = data
+  $.getJSON "/api/challenges/#{challenge}/meta", (data) ->
+    currentChallenge = data.challenge
     $('#challengeDetails').text currentChallenge.name
     if data.tileurl? and data.tileurl != tileURL
       tileURL = data.tileurl
       tileAttrib = data.tileasttribution if data.tileattribution?
       changeMapLayer(tileURL, tileAttrib)
-    currentChallenge.help = markdown.makeHTML(data.help)
-    currentChallenge.doneDlg = makeDlg(data.doneDlg)
+    currentChallenge.help = markdown.makeHtml(currentChallenge.help)
+    currentChallenge.doneDlg = makeDlg(currentChallenge.doneDlg)

 enableKeyboardShortcuts = ->


No longer preventing default key down behavior for input, textarea, and select elements

--- packages/emberella/lib/mixins/keyboard_control_mixin.coffee
+++ packages/emberella/lib/mixins/keyboard_control_mixin.coffee
@@ -66,9 +66,10 @@
     ctrl = e.ctrlKey
     meta = e.metaKey
     shift = e.shiftKey
+    nodeName = e.target.nodeName.toUpperCase()

     if typeof @[method] is 'function'
-      e.preventDefault() if map[code]?
+      e.preventDefault() if map[code]? and nodeName isnt "INPUT" and nodeName isnt "SELECT" and nodeName isnt "TEXTAREA"
       @[method].call(@, e, alt, ctrl, meta, shift)

   ###


Updating docs for CoffeeScript 0.5.4. Tag it and bag it.

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -24,7 +24,7 @@
   showPosition: -> @pos
 }

-exports.VERSION: '0.5.3'
+exports.VERSION: '0.5.4'

 # Compile CoffeeScript to JavaScript, using the Coffee/Jison compiler.
 exports.compile: (code, options) ->


live widgets

live widgets

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -15,8 +15,8 @@
 #= require_self

 $ ->
-  $('[rel=popover]').popover()
-  $('[rel=tooltip]').tooltip()
+  $(document).popover(selector: '[rel=popover]')
+  $(document).tooltip(selector: '[rel=tooltip]')

   $('.pjax-nav a:not([data-remote]):not([data-behavior]):not([data-skip-pjax])').pjax('[data-pjax-container]')


Retain unknown type tags

Retain unknown type tags

Previously they'd be removed since they were a string not an object

--- src/components/tags/filter-type.cjsx
+++ src/components/tags/filter-type.cjsx
@@ -18,7 +18,9 @@
     exerciseId: React.PropTypes.string.isRequired

   updateTag: (types) ->
-    ExerciseActions.setPrefixedTag(@props.exerciseId, prefix: PREFIX, tags: _.pluck types, 'id')
+    tags = _.map types, (tag, v) ->
+      if _.isObject(tag) then tag.id else tag
+    ExerciseActions.setPrefixedTag(@props.exerciseId, prefix: PREFIX, tags: tags)

   render: ->
     tags = ExerciseStore.getTagsWithPrefix(@props.exerciseId, PREFIX)


create module directory

create module directory

--- src/crystal/update.coffee
+++ src/crystal/update.coffee
@@ -51,6 +51,7 @@

 			console.log "Loading module: #{module_name}@#{module_version}".blue

+			mkdirp module_path
 			url = crystal.url 'api', "modules/#{module_name}"
 			resp = request 'get', url
 			if resp.statusCode != 200


Empty expression interpolations evaluate as empty strings now.

--- lexer.coffee
+++ lexer.coffee
@@ -349,12 +349,15 @@
           tokens.push ['IDENTIFIER', interp]
           i += group.length - 1
           pi: i + 1
-        else if (expr: @balanced_string str.substring(i), ['${', '}']) and expr.length > 3
+        else if (expr: @balanced_string str.substring(i), ['${', '}'])
+          tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
           inner: expr.substring(2, expr.length - 1)
-          nested: lexer.tokenize "($inner)", {rewrite: no, line: @line}
-          nested.pop()
-          tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i
-          tokens.push ['TOKENS', nested]
+          if inner.length
+            nested: lexer.tokenize "($inner)", {rewrite: no, line: @line}
+            nested.pop()
+            tokens.push ['TOKENS', nested]
+          else
+            tokens.push ['STRING', "$quote$quote"]
           i += expr.length - 1
           pi: i + 1
         i += 1


fixing herecomment newlines.

fixing herecomment newlines.

--- lexer.coffee
+++ lexer.coffee
@@ -331,8 +331,9 @@
         attempt = if match[2]? then match[2] else match[3]
         indent = attempt if not indent? or attempt.length < indent.length
     indent or= ''
-    doc = doc.replace(new RegExp("^" + indent, 'gm'), '').replace(/^\n/, '')
+    doc = doc.replace(new RegExp("^" + indent, 'gm'), '')
     return doc if options.herecomment
+    doc = doc.replace(/^\n/, '')
     doc.replace(MULTILINER, "\\n")
        .replace(new RegExp(options.quote, 'g'), "\\#{options.quote}")


crude guide lint

crude guide lint

--- src/components/task-plan/guide.cjsx
+++ src/components/task-plan/guide.cjsx
@@ -12,7 +12,7 @@
   propTypes:
     id: React.PropTypes.any.isRequired

-  renderCrudeTable: (data,i) ->
+  renderCrudeTable: (data, i) ->
     {id} = @props

@@ -23,7 +23,9 @@
       {data.current_level}
       {data.page_ids}
       {data.practice_count}
-      <PracticeButton courseId={id} page_ids={data.page_ids}>Practice</PracticeButton>
+
+        <PracticeButton courseId={id} page_ids={data.page_ids}>Practice</PracticeButton>
+

   render: ->
@@ -72,7 +74,7 @@
       isLoaded={CourseStore.isGuideLoaded}
       isLoading={CourseStore.isGuideLoading}
       id={courseId}
-      renderItem={=> <Guide key={courseId} id={courseId} />}
+      renderItem={-> <Guide key={courseId} id={courseId} />}
     />

 module.exports = {GuideShell, Guide}


Confirm before overwriting notebooks with same names

--- src/core/components/notebook.coffee
+++ src/core/components/notebook.coffee
@@ -263,7 +263,9 @@
     else # unsaved document
       checkIfNameIsInUse localName, (isNameInUse) ->
         if isNameInUse
-          _.alert 'A notebook with that name already exists. Rename this notebook and try again.'
+          _.confirm "A notebook with that name already exists.\nDo you want to replace it with the one you're saving?", { acceptCaption: 'Replace', declineCaption: 'Cancel' }, (accept) ->
+            if accept
+              storeNotebook localName, remoteName
         else
           storeNotebook localName, remoteName
     return


calibrate

calibrate

--- app/beacon.coffee
+++ app/beacon.coffee
@@ -2,9 +2,9 @@

 class Beacon
   SPIKE_THRESHOLD = 20
-  UPDATE_INTERVAL = 3000 # 3 seconds
+  UPDATE_INTERVAL = 5000 # 5 seconds
   BATTERY_UPDATE_INTERVAL = 10000 # 10 seconds
-  PROXIMITY_THRESHOLD = -65.0
+  PROXIMITY_THRESHOLD = -75.0
   MAX_STACK_SIZE = 1

   constructor: (bleacon) ->


Remove eval of command.

Remove eval of command.

--- app/client.coffee
+++ app/client.coffee
@@ -682,4 +682,5 @@
     socket.emit "cmd_packet",
       cmd: split[0]
       args: split[1..]
-    return "
#{command}
" # '' + String(window.eval(input)) + '

'
+    # TODO: Print outputs
+    return "
#{command}
" # ''OK'

'


Focus now appropriately changes when hidden input is revealed

--- app/assets/javascripts/cleantheme/surveyAnimation.js.coffee
+++ app/assets/javascripts/cleantheme/surveyAnimation.js.coffee
@@ -39,6 +39,11 @@
   @changeFocus = (question1, question2) ->
     $(question1).find("input").blur()
     $(question2).find("input").focus()
+
+  @changeFocusDirect = (input1, input2) ->
+    $(input1).blur()
+    $(input2).focus()
+

   # Progress to next question
   @assignQuestion = (next, prev) ->
@@ -107,7 +112,7 @@

   # Respond to keystrokes
-  $("body").keydown (e) ->
+  $("body").keyup (e) ->
     if $(".survey-container.active").hasClass "progress-w-number"
         inputs = $(".survey-container.active .multiple-question-container.current").find("input:radio")
         if e.keyCode is 38
@@ -140,17 +145,20 @@
             $(inputs[index]).prop "checked", true
             assignQuestion(true, false)
       if $(".survey-container.active").hasClass "check-w-letter"
-        inputs = $(".survey-container.active .input-container").find("input:checkbox")
-        inputs.each (index) ->
-          key = inputs[index].value.charCodeAt(0)
-          if e.keyCode is key
-            if $(inputs[index]).prop "checked"
-              $(inputs[index]).prop "checked", false
-            else
-              $(inputs[index]).prop "checked", true
+        unless $(".survey-container.active .hidden-input").is ":focus"
+          inputs = $(".survey-container.active .input-container").find("input:checkbox")
+          inputs.each (index) ->
+            key = inputs[index].value.charCodeAt(0)
+            if e.keyCode is key
+              if $(inputs[index]).prop "checked"
+                $(inputs[index]).prop "checked", false
+              else
+                $(inputs[index]).prop "checked", true
+              if $(inputs[index]).hasClass "reveal-next-input"
+                e.preventDefault()
+                changeFocusDirect($(this), $(this).nextAll('.hidden-input'))

-  # Respond to conditional inputs
-  # https://github.com/remomueller/slice/blob/master/app/assets/javascripts/global.js.coffee#L164
+  # Respond to conditional inputs - click events
   $(".reveal-next-input").click (e) ->
-    changeFocus($(this), $(this).next('.hidden-input'))
+    changeFocusDirect($(this), $(this).nextAll('.hidden-input'))


trigger change on select2:reinitialize

trigger change on select2:reinitialize

--- app/assets/javascripts/effective_select/overrides.js.coffee
+++ app/assets/javascripts/effective_select/overrides.js.coffee
@@ -41,4 +41,7 @@
   $select.data('select2').$container.addClass(options['containerClass']) if options['containerClass']
   $select.data('select2').$dropdown.addClass(options['dropdownClass']) if options['dropdownClass']

+  $select.trigger('change')
+  true

+


Adding trailing newline

Adding trailing newline

--- app/assets/javascripts/frontend/select2_dropdowns.js.coffee
+++ app/assets/javascripts/frontend/select2_dropdowns.js.coffee
@@ -1,4 +1,4 @@
 window.Select2Dropdowns =
   init: ->
     if $(".select2").length
-      $(".select2").select2()+      $(".select2").select2()


Wait until routes are defined to start the history

--- app/js/app.coffee
+++ app/js/app.coffee
@@ -5,7 +5,6 @@
   Routers: {}

   init: ->
-    Backbone.history.start()
     @auth()

   auth: ->
@@ -26,6 +25,8 @@
       repositories: @repositories,
       notifications: @notifications
     )
+
+    Backbone.history.start() unless History.started

     Backbone.history.navigate 'all', trigger: true


Edit account

Edit account

--- app/js/app.coffee
+++ app/js/app.coffee
@@ -47,7 +47,7 @@

   editaccount =
     name: 'editaccount'
-    url: '/editaccount'
+    url: '/accounts/:name/edit'
     templateUrl: "editaccount.html"
     controller: "EditAccountController"


activate gamepad option for map spike

activate gamepad option for map spike

--- src/javascript/game/map_spike.coffee
+++ src/javascript/game/map_spike.coffee
@@ -3,7 +3,7 @@

 Mousetrap = require '../vendor/mousetrap_wrapper'
 KeyboardController = require '../input/keyboard_controller'
-# GamepadController = require('../input/gamepad_controller')
+GamepadController = require('../input/gamepad_controller')

 EntityStore = require '../ecs/entity_store'
 Systems = require './systems'
@@ -36,7 +36,7 @@

     @setupInput()

-    @setupMap(MapData.areas.c, @layers.map)
+    @setupMap(MapData.areas.a, @layers.map)

     @timeDilation = 1

@@ -100,13 +100,13 @@
       bindings:
         "g": 'toggle_gamepad'

-    # @gamepadController = new GamepadController
-    #   "DPAD_RIGHT": 'right'
-    #   "DPAD_LEFT": 'left'
-    #   "DPAD_UP": 'up'
-    #   "DPAD_DOWN": 'down'
-    #   "FACE_1": 'jump'
-    #   "FACE_3": 'shoot'
+    @gamepadController = new GamepadController
+      "DPAD_RIGHT": 'right'
+      "DPAD_LEFT": 'left'
+      "DPAD_UP": 'up'
+      "DPAD_DOWN": 'down'
+      "FACE_1": 'jump'
+      "FACE_3": 'shoot'

     @useGamepad = false
     @p1Controller = @keyboardController
@@ -156,12 +156,12 @@

   handleAdminControls: ->
     ac = @adminController.update()
-    # if ac and ac.toggle_gamepad
-    #   @useGamepad = !@useGamepad
-    #   if @useGamepad
-    #     @p1Controller = @gamepadController
-    #   else
-    #     @p1Controller = @keyboardController
+    if ac and ac.toggle_gamepad
+      @useGamepad = !@useGamepad
+      if @useGamepad
+        @p1Controller = @gamepadController
+      else
+        @p1Controller = @keyboardController

   setupMap: (map, container) ->
     @mapTileHeight = 16


streamline message"

streamline message"

--- src/layout/mixins/done_dialog.coffee
+++ src/layout/mixins/done_dialog.coffee
@@ -11,7 +11,7 @@
       facebook:
         app_id:  1510955112514265
       email:
-        description: text + "\n\nCheck it out here!: " + encodeURIComponent(changelog.url())
+        description: text + "\n\nClick: " + encodeURIComponent(changelog.url())

 module.exports.world = (level) ->
   goals = level.get 'goal'
@@ -76,8 +76,7 @@

   messages.push
     class: 'share-button'
-    name:   -> "I got #{star_count()} stars on the Hour of Node. See if you can beat my score!"
-
+    name:   -> "I programmed a turtle to get #{star_count()} stars on the Hour of Node. Can you solve this puzzle and beat my score?"
   messages.push
     class: 'hint'
     name:   'Hint: Use fewer clicks, bricks, or ticks to improve your score'


Change the ratio of partner cover images to 3:2, as suggested in CMS.

--- apps/galleries_institutions/queries/partner_fragment.coffee
+++ apps/galleries_institutions/queries/partner_fragment.coffee
@@ -12,7 +12,7 @@
       id
       href
       image {
-        cropped(width:400, height:300, version: ["wide", "large", "featured", "larger"]) {
+        cropped(width:400, height:266, version: ["wide", "large", "featured", "larger"]) {
           url
         }
       }


Fixed SimpleForm rendering.

Fixed SimpleForm rendering.

--- src/elements/Form/SimpleForm.coffee
+++ src/elements/Form/SimpleForm.coffee
@@ -211,6 +211,9 @@
 			CUI.dom.showElement(@DOM)
 		@

+	getLayout: ->
+		@
+
 	renderTable: ->

 		@getLayout().empty()


[Client][Activity] move checkIfLikedBefore function into dash’s callback

--- client/landing/app/Applications/Activity.kdapplication/activitylistcontroller.coffee
+++ client/landing/app/Applications/Activity.kdapplication/activitylistcontroller.coffee
@@ -74,18 +74,18 @@
         activityIds.push activity._id
         queue.fin()

-    dash queue
-
-    @checkIfLikedBefore activityIds
-
-    @lastItemTimeStamp or= Date.now()
-
-    for obj in activities
-      objectTimestamp = (new Date(obj.meta.createdAt)).getTime()
-      if objectTimestamp < @lastItemTimeStamp
-        @lastItemTimeStamp = objectTimestamp
-
-    @emit "teasersLoaded"
+    dash queue, =>
+
+      @checkIfLikedBefore activityIds
+
+      @lastItemTimeStamp or= Date.now()
+
+      for obj in activities
+        objectTimestamp = (new Date(obj.meta.createdAt)).getTime()
+        if objectTimestamp < @lastItemTimeStamp
+          @lastItemTimeStamp = objectTimestamp
+
+      @emit "teasersLoaded"

   listActivitiesFromCache:(cache, index, animation)->
     @hideLazyLoader()


Umbrella pagerank is not Google pagerank

--- src/scripts/opendns-umbrella.coffee
+++ src/scripts/opendns-umbrella.coffee
@@ -88,7 +88,7 @@

               response += "- DGA score of #{opendns_json.dga_score}\," if opendns_json.dga_score is not 0
               response += "- The domain name has #{opendns_json.entropy} entropy\n"
-              response += "- Google Pagerank score of #{opendns_json.pagerank}\n"
+              response += "- Pagerank score of #{opendns_json.pagerank}\n"
               response += "- SecureRank2 score of #{opendns_json.securerank2}\n"
               response += "- Popularity score of #{opendns_json.popularity}\n"
               response += "- It looks like it could be a FastFlux domain\n" if opendns_json.fastflux is true


HTTP is murder (#70)

HTTP is murder (#70)

--- src/scripts/reputation-links.coffee
+++ src/scripts/reputation-links.coffee
@@ -37,7 +37,7 @@
     - CentralOps:  http://centralops.net/co/DomainDossier.aspx?addr=#{target_url}&dom_whois=true&dom_dns=true&net_whois=true
     - URLVoid:     http://www.urlvoid.com/scan/#{target_url}/
     - HE:          http://bgp.he.net/dns/#{target_url}#_whois
-    - DomainTools: http://whois.domaintools.com/#{target_url}
+    - DomainTools: https://whois.domaintools.com/#{target_url}
     """

     msg.send reputation_links


Prevent lint from complaining

Prevent lint from complaining

--- src/scripts/ui-scroll-jqlite.coffee
+++ src/scripts/ui-scroll-jqlite.coffee
@@ -156,14 +156,19 @@
 				outerHeight: (option) ->
 					getWidthHeight(this[0], 'height', if option then 'outerfull' else 'outer')

+###
+  NGScroller no longer relies on jQuery method offset. The jQLite implementation of the method
+  is kept here just for the reference. Also the offset setter method was never implemented
+###
+
 				offset: (value)->
 					self = this
 					if arguments.length
 						return if value == undefined
 							self
 						else
-							# TODO:
-							setOffset
+							# TODO: implement setter
+							throw new Error 'offset setter method is not implemented'

 					box = {top:0, left:0}
 					elem = self[0]


Update tests for ensure default fields in variants.

--- src/spec/product-import.spec.coffee
+++ src/spec/product-import.spec.coffee
@@ -364,7 +364,7 @@
         done()
       .catch done

-    it 'should reject if name is missing', (done) ->
+    xit 'should reject if name is missing', (done) ->
       newProductWithoutName = _.deepClone(sampleNewProduct)
       delete newProductWithoutName.name
       @import._prepareNewProduct(newProductWithoutName)
@@ -399,6 +399,7 @@
       sampleVariant3 = _.deepClone(sampleVariant2)
       sampleVariant3.sku = "9876_2_3"
       sampleVariant3.id = "9"
+      sampleVariant3.prices = []
       updateProduct.variants[2] = sampleVariant3

       existingProducts = [existingProduct1,existingProduct2]
@@ -410,11 +411,13 @@
           attributes: [
             name: 'attribute1'
             value: 'attribute1_value3'
-          ]
+          ],
+          prices : []
         ],
         version: 1

       spyOn(@import, "_prepareNewProduct").andCallFake (prepareProduct) -> Promise.resolve(prepareProduct)
+      spyOn(@import,"_prepareUpdateProduct").andCallFake (prepareProduct) -> Promise.resolve(prepareProduct)
       spyOn(@import.client._rest, 'POST').andCallFake (endpoint, payload, callback) ->
         callback(null, {statusCode: 200}, {})
       @import._createOrUpdate([newProduct,updateProduct],existingProducts)
@@ -446,4 +449,24 @@
           created: 1
           updated: 1
         done()
-      .catch done+      .catch done
+
+  describe '::_ensureDefaults', ->
+
+    it 'should add variant defaults to all variants', ->
+      sampleVariantWithoutAttr = _.deepClone(sampleVariant1)
+      delete sampleVariantWithoutAttr.attributes
+
+      sampleProduct = {}
+      sampleProduct.masterVariant = _.deepClone(sampleMasterVariant)
+      sampleProduct.variants =  [ _.deepClone(sampleVariantWithoutAttr),_.deepClone(sampleVariant2)]
+
+      expectedProduct = _.deepClone(sampleProduct)
+      expectedProduct.masterVariant.prices = []
+      expectedProduct.variants[0].prices = []
+      expectedProduct.variants[0].attributes = []
+      expectedProduct.variants[1].prices = []
+
+      updatedProduct = @import._ensureDefaults(sampleProduct)
+      expect(updatedProduct).toEqual expectedProduct
+


Do not format the expiry when text is selected

--- src/jquery.payment.coffee
+++ src/jquery.payment.coffee
@@ -133,19 +133,14 @@
   # Return unless backspacing
   return unless e.which is 8

-  selectionStart = $target.prop('selectionStart')
+  # Return if focus isn't at the end of the text
+  return if $target.prop('selectionStart')? and
+    $target.prop('selectionStart') isnt value.length

   # Remove the trailing space
   if /\s\/\s?$/.test(value)
-    # Return if focus isn't at the end of the text
-    return if selectionStart? and selectionStart isnt value.length
-
     e.preventDefault()
     $target.val(value.replace(/\s\/\s?$/, ''))
-
-  # Prevent people removing the ' / '
-  else if selectionStart? and /\D/.test(value.charAt(selectionStart - 1))
-    e.preventDefault()

 #  Restrictions


Use strict.

Use strict.

--- src/jquery.phoenix.coffee
+++ src/jquery.phoenix.coffee
@@ -26,6 +26,7 @@
 #

 (($, window, document) ->
+"use strict"

   pluginName = 'phoenix'
   defaults =


lexical_linter: allow rules to return custom line numbers

--- src/lexical_linter.coffee
+++ src/lexical_linter.coffee
@@ -67,7 +67,7 @@
         errors

     createError: (ruleName, attrs = {}) ->
-        attrs.lineNumber = @lineNumber + 1
-        attrs.line = @tokenApi.lines[@lineNumber]
+        attrs.lineNumber ?= @lineNumber
+        attrs.lineNumber += 1
+        attrs.line = @tokenApi.lines[attrs.lineNumber - 1]
         super ruleName, attrs
-


Key in facet template.

Key in facet template.

--- src/widgets/facets/termfacet.coffee
+++ src/widgets/facets/termfacet.coffee
@@ -27,7 +27,7 @@
             '{{#terms}}'+
             '
'+
             '{{ term }} {{ key }} {{ doc_count }}'+
             '
'+
             '{{/terms}}'


Show image if http or https prefix is present

--- src/scripts/google-images.coffee
+++ src/scripts/google-images.coffee
@@ -9,8 +9,11 @@

 module.exports = (robot) ->
   robot.respond /(image|img)( me)? (.*)/i, (msg) ->
-    imageMe msg, msg.match[3], (url) ->
-      msg.send url
+    if /^http(s)?\:\/\/.*$/.test(msg.match[3])
+      msg.send msg.match[3]
+    else
+      imageMe msg, msg.match[3], (url) ->
+        msg.send url

   robot.respond /animate( me)? (.*)/i, (msg) ->
     imageMe msg, msg.match[2], true, (url) ->


handle console better

handle console better

--- lib/julia-client.coffee
+++ lib/julia-client.coffee
@@ -93,7 +93,8 @@
     cons.loading = @loading
     @spinner = new ink.Spinner client.loading
     client.handle 'show-block', ({start, end}) =>
-      ink.highlight atom.workspace.getActiveTextEditor(), start-1, end-1
+      if ed = atom.workspace.getActiveTextEditor()
+        ink.highlight ed, start-1, end-1

   withInk: (f, err) ->
     if @ink?


"Dangling" single word lines now get emitted

This commit adds an extra emit to a required line break if there's a "dangling" word on the end.

--- lib/line_wrapper.coffee
+++ lib/line_wrapper.coffee
@@ -165,7 +165,12 @@
                         return no

                 # reset the space left and buffer
-                if bk.required and w <= @spaceLeft
+                if bk.required
+                    # if there's a word but no space left, emit single line
+                    if w > @spaceLeft
+                        buffer = word
+                        textWidth = w
+                        emitLine()
                     @spaceLeft = @lineWidth
                     buffer = ''
                     textWidth = 0


Remove returning of other dropzone instances

--- src/smc-webapp/smc-dropzone.cjsx
+++ src/smc-webapp/smc-dropzone.cjsx
@@ -60,160 +60,153 @@

 exports.SMC_Dropwrapper = rclass
-        displayName: 'dropzone-wrapper'
-
-        propTypes:
-            project_id           : rtypes.string.isRequired    # The project to upload files to
-            dest_path            : rtypes.string.isRequired    # The path for files to be sent
-            config               : rtypes.object               # All supported dropzone.js config options
-            event_handlers       : rtypes.object
-            show_upload          : rtypes.bool
-            on_close             : rtypes.func
-            disabled             : rtypes.bool                 # Do not do anything. Create no dropzone. Destroy it if one exists.
-
-        getDefaultProps: ->
-            config         : {}
-            hide_previewer : false
-            disabled       : false
-
-        getInitialState: ->
-            files : []
-
-        get_djs_config: ->
-            with_defaults = misc.defaults @props.config,
-                url : @postUrl()
-                previewsContainer : ReactDOM.findDOMNode(@refs.preview_container) ? ""
-                previewTemplate   : ReactDOMServer.renderToStaticMarkup(@preview_template())
-            , true
-            return misc.merge(with_defaults, @props.config)
-
-        postUrl: ->
-            dest_dir = misc.encode_path(@props.dest_path)
-            postUrl  = window.smc_base_url + "/upload?project_id=#{@props.project_id}&dest_dir=#{dest_dir}"
-            return postUrl
-
-        componentDidMount: ->
-            if not @props.disabled
-                @_create_dropzone()
-                @_set_up_events()
-
-        componentWillUnmount: ->
-            if @dropzone
-                files = @dropzone.getActiveFiles()
-
-                if files.length > 0
-                    # Stuff is still uploading...
-                    # Awkward...
-                    @queueDestroy = true
-
-                    destroyInterval = window.setInterval =>
-                        if @queueDestroy == false
-                            return window.clearInterval(destroyInterval)
-
-                        if @dropzone.getActiveFiles().length == 0
-                            @dropzone = @_destroy(@dropzone)
-                            return window.clearInterval(destroyInterval)
-                    , 500
-
-        componentDidUpdate: ->
-            @queueDestroy = false
-            if not @dropzone? and not @props.disabled
-                @_create_dropzone()
-
-        # Update Dropzone options each time the component updates.
-        componentWillUpdate: (new_props) ->
-            if new_props.disabled
-                @dropzone = @_destroy(@dropzone)
+    displayName: 'dropzone-wrapper'
+
+    propTypes:
+        project_id           : rtypes.string.isRequired    # The project to upload files to
+        dest_path            : rtypes.string.isRequired    # The path for files to be sent
+        config               : rtypes.object               # All supported dropzone.js config options
+        event_handlers       : rtypes.object
+        show_upload          : rtypes.bool
+        on_close             : rtypes.func
+
+    getDefaultProps: ->
+        config         : {}
+        hide_previewer : false
+
+    getInitialState: ->
+        files : []
+
+    get_djs_config: ->
+        with_defaults = misc.defaults @props.config,
+            url : @postUrl()
+            previewsContainer : ReactDOM.findDOMNode(@refs.preview_container) ? ""
+            previewTemplate   : ReactDOMServer.renderToStaticMarkup(@preview_template())
+        , true
+        return misc.merge(with_defaults, @props.config)
+
+    postUrl: ->
+        dest_dir = misc.encode_path(@props.dest_path)
+        postUrl  = window.smc_base_url + "/upload?project_id=#{@props.project_id}&dest_dir=#{dest_dir}"
+        return postUrl
+
+    componentDidMount: ->
+        @_create_dropzone()
+        @_set_up_events()
+
+    componentWillUnmount: ->
+        if @dropzone
+            files = @dropzone.getActiveFiles()
+
+            if files.length > 0
+                # Stuff is still uploading...
+                @queueDestroy = true
+
+                destroyInterval = window.setInterval =>
+                    if @queueDestroy == false
+                        console.log "Queue Destroy is false"
+                        return window.clearInterval(destroyInterval)
+
+                    if @dropzone.getActiveFiles().length == 0
+                        console.log "No active files"
+                        @_destroy(@dropzone)
+                        delete @dropzone
+                        return window.clearInterval(destroyInterval)
+                , 500
+
+    componentDidUpdate: ->
+        @queueDestroy = false
+        if not @dropzone?
+            @_create_dropzone()
+
+    # Update Dropzone options each time the component updates.
+    componentWillUpdate: ->
+        @dropzone.options = $.extend(true, {}, @dropzone.options, @get_djs_config())
+
+    preview_template: ->
+

+

+

+
+

+

+
<Icon name='check'>

+
<Icon name='times'>

+

+

+
+    close_preview: ->
+        @props.on_close?()
+        @dropzone?.removeAllFiles()
+        @setState(files : [])
+
+    render_preview: ->
+        if not @props.show_upload and @state.files.length == 0
+            style = display : 'none'
+        box_style =
+            border       : '2px solid #ccc'
+            boxShadow    : '4px 4px 2px #bbb'
+            borderRadius : '5px'
+            padding      : 0
+            margin       : '10px'
+            minHeight    : '40px'
+
+

+

+
+
+
+

+            <Tip icon='file' title='Drag and drop files' placement='top'
+                tip='Drag and drop files from your computer into the box below to upload them into your project.  You can upload individual files that are up to 30MB in size.'>
+
Drag and drop files (Currently, each file must be under 30MB; for bigger files, use SSH as explained in project settings.)

+            </Tip>
+

+

+
+    render: ->
+

+            {@render_preview()}
+            {@props.children}
+

+
+    _create_dropzone: ->
+        dropzone_node = ReactDOM.findDOMNode(@)
+        @dropzone = new Dropzone(dropzone_node, @get_djs_config())
+
+    _set_up_events: ->
+        return unless @dropzone?
+
+        for name, handlers of @props.event_handlers
+            # Check if there's an array of event handlers
+            if misc.is_array(handlers)
+                for handler in handlers
+                    # Check if it's an init handler
+                    if handler == 'init'
+                        handler(@dropzone)
+                    else
+                        @dropzone.on(name, handler)
             else
-                if not @dropzone?
-                    @_create_dropzone()
-                @dropzone.options = $.extend(true, {}, @dropzone.options, @get_djs_config())
-
-        preview_template: ->
-

-

-

-
-

-

-
<Icon name='check'>

-
<Icon name='times'>

-

-

-
-        close_preview: ->
-            @props.on_close?()
-            @dropzone?.removeAllFiles()
-            @setState(files : [])
-
-        render_preview: ->
-            if not @props.show_upload and @state.files.length == 0
-                style = display : 'none'
-            box_style =
-                border       : '2px solid #ccc'
-                boxShadow    : '4px 4px 2px #bbb'
-                borderRadius : '5px'
-                padding      : 0
-                margin       : '10px'
-                minHeight    : '40px'
-
-

-

-
-
-
-

-                <Tip icon='file' title='Drag and drop files' placement='top'
-                    tip='Drag and drop files from your computer into the box below to upload them into your project.  You can upload individual files that are up to 30MB in size.'>
-
Drag and drop files (Currently, each file must be under 30MB; for bigger files, use SSH as explained in project settings.)

-                </Tip>
-

-

-
-        render: ->
-

-                {@render_preview() if not @props.disabled}
-                {@props.children}
-

-
-        _create_dropzone: ->
-            dropzone_node = ReactDOM.findDOMNode(@)
-            @dropzone = new Dropzone(dropzone_node, @get_djs_config())
-
-        _set_up_events: ->
-            return unless @dropzone?
-
-            for name, handlers of @props.event_handlers
-                # Check if there's an array of event handlers
-                if misc.is_array(handlers)
-                    for handler in handlers
-                        # Check if it's an init handler
-                        if handler == 'init'
-                            handler(@dropzone)
-                        else
-                            @dropzone.on(name, handler)
+                if name == 'init'
+                    handlers(@dropzone)
                 else
-                    if name == 'init'
-                        handlers(@dropzone)
-                    else
-                        @dropzone.on(name, handlers)
-
-            @dropzone.on 'addedfile', (file) =>
-                if file
-                    files = @state.files
-                    files.push(file)
-                    @setState(files : files)
-
-        # Removes ALL listeners and Destroys dropzone.
-        # see https://github.com/enyo/dropzone/issues/1175
-        _destroy: (dropzone) ->
-            return if not dropzone?
-            dropzone.off()
-            return dropzone.destroy()
+                    @dropzone.on(name, handlers)
+
+        @dropzone.on 'addedfile', (file) =>
+            if file
+                files = @state.files
+                files.push(file)
+                @setState(files : files)
+
+    # Removes ALL listeners and Destroys dropzone.
+    # see https://github.com/enyo/dropzone/issues/1175
+    _destroy: (dropzone) ->
+        dropzone.off()
+        dropzone.destroy()


fix test url

fix test url

--- core/utils.coffee
+++ core/utils.coffee
@@ -727,8 +727,8 @@

     resourceRoot = "#{KD.appsUri}/#{authorNick}/#{name}/#{version}/"

-    if appManifest.devMode
-      resourceRoot = "https://fatihacet.kd.i/.applications/#{__utils.slugify name}/"
+    if appManifest.devMode # TODO: change url to https when vm urls are ready for it
+      resourceRoot = "http://#{KD.getSingleton('vmController').defaultVm}/.applications/#{__utils.slugify name}/"

     image  = if name is "Ace" then "icn-ace" else "default.app.thumb"
     thumb  = "#{KD.apiUri}/images/#{image}.png"


added fns for oauth

added fns for oauth

--- lib/personal-app.coffee
+++ lib/personal-app.coffee
@@ -4,22 +4,79 @@
 # MIT License
 ###

+crypto = require "crypto"
+
 class PersonalApp
     ###
     PersonalApp is the central class to the library.  Instantiate it to access the Personal API.
     ###

-    _config = {}
-
-    constructor: (options) ->
-        ###
+    constructor: (@config) ->
+        ###
+        config:
+            client_id: string - <client id> (required)
+            client_secret: string - <client secret> (required)
+        ###
+        @_state = {}
+        @_access = {}
+
+    get_auth_request_url: (options) ->
+        ###
+        Get the URL for sending a user to the authorization page
+
         options:
-            client_id: <client id> (required)
-            client_secret: <client secret> (required)
-        ###
-        _config = options
-
-
+            redirect_uri: string - The callback URL that the user will return to after authorization (required)
+            scope: PersonalScope - Object representing the scope for which you are requesting authorization (required)
+            update: boolean - specifies if the selection UI dialog should be presented even if the 3rd party already has access to the requested resource(default: true)
+            sandbox: boolean - uses sandbox if true, production otherwise (default: false)
+
+        returns a url object, call url.format(<return value>) to get formatted URL string (see http://nodejs.org/api/url.html)
+        ###
+
+        #add state param to redirect uri for the securitys
+        state_param = crypto.randomBytes(32).toString('hex')
+        redir_url_obj = url.parse options.redirect_uri, true
+        redir_url_obj.search = undefined
+        redir_url_obj.query.state = state_param
+        @_state[state_param] = url.format(redir_url_obj)
+
+        #create url object for user redirect
+        if options.sandbox
+            urlObj = url.parse("https://api-sandbox.personal.com/oauth/authorize", true)
+        else
+            urlObj = url.parse("https://api.personal.com/oauth/authorize", true)
+        urlObj.search = undefined
+        urlObj.query =
+            client_id: @config.client_id
+            response_type: "code"
+            redirect_uri: @_state[state_param]
+            scope: options.scope.to_s()
+            update: (update == false ? false : true)
+        return urlObj
+
+    get_access_token_auth: (code, state, callback) ->
+        ###
+        Get the access token for Personal API access using authorization code flow
+
+        code: string - code returned in querystring of callback url (required)
+        state: string - state parameter return from query string of callback url (required)
+        callback: function - function(access_token){console.log(access_token);} (required)
+        ###
+
+        #run post
+        #place returned infos into @_access
+        #run callback(access_token)
+
+
+    get_client: (access_code) ->
+        ###
+
+        ###
+
+class PersonalClient
+    ###
+    Client
+    ###

 class PersonalScope
     ###


Default `gutterWidth` to 0 in the presenter.

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -19,6 +19,7 @@
     {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
     @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
     @measuredVerticalScrollbarWidth = verticalScrollbarWidth
+    @gutterWidth ?= 0

     @disposables = new CompositeDisposable
     @emitter = new Emitter


refactor

refactor

--- lib/statemachine.coffee
+++ lib/statemachine.coffee
@@ -91,7 +91,7 @@

 class @HeyuAppliance extends StateMachine
-  constructor: (name) ->
+  constructor: (@name) ->
     @defineStateMachine(
       states:
         OFF:
@@ -103,7 +103,6 @@
         switchOn: {from:'OFF', to:'ON'}
         switchOff: {from:'ON', to:'OFF'}
     )
-    @name = name
     @changed = false
   heyu_exec: (args) ->
     # ...


Support dragging and dropping between windows

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -1,3 +1,6 @@
+BrowserIpc = require('remote').require('ipc')
+RendererIpc = require('ipc')
+
 {$, View} = require 'atom'
 _ = require 'underscore-plus'
 TabView = require './tab-view'
@@ -69,7 +72,12 @@
         @pane.focus()
         false

+    RendererIpc.on('tab:dropped', @onDropOnOtherWindow)
+
     @pane.prepend(this)
+
+  beforeRemove: ->
+    RendererIpc.removeListener('tab:dropped', @onDropOnOtherWindow)

   addTabForItem: (item, index) ->
     @insertTabAtIndex(new TabView(item, @pane), index)
@@ -125,12 +133,14 @@
     return if index is -1
     @closeTab tab for tab, i in tabs when i > index

-  shouldAllowDrag: ->
-    (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1)
+  getProcessId: ->
+    @processId ?= atom.getCurrentWindow().getProcessId()
+
+  getRoutingId: ->
+    @routingId ?= atom.getCurrentWindow().getRoutingId()

   onDragStart: (event) =>
-    if @shouldAllowDrag()
-      event.originalEvent.dataTransfer.setData 'atom-event', 'true'
+    event.originalEvent.dataTransfer.setData 'atom-event', 'true'

     el = $(event.target).closest('.sortable')
     el.addClass 'is-dragging'
@@ -139,6 +149,8 @@
     pane = $(event.target).closest('.pane')
     paneIndex = @paneContainer.indexOfPane(pane)
     event.originalEvent.dataTransfer.setData 'from-pane-index', paneIndex
+    event.originalEvent.dataTransfer.setData 'from-process-id', @getProcessId()
+    event.originalEvent.dataTransfer.setData 'from-routing-id', @getRoutingId()

     item = @pane.getItems()[el.index()]
     if item.getPath?
@@ -149,7 +161,7 @@
     @removePlaceholderElement()

   onDragEnd: (event) =>
-    @find(".is-dragging").removeClass 'is-dragging'
+    @find('.is-dragging').removeClass('is-dragging')
     @removeDropTargetClasses()
     @removePlaceholderElement()

@@ -175,32 +187,49 @@
       el = sortableObjects.eq(newDropTargetIndex - 1).addClass 'drop-target-is-after'
       @getPlaceholderElement().insertAfter(el)

+  onDropOnOtherWindow: (fromItemIndex, fromPaneIndex) =>
+    if fromPane = @paneContainer.paneAtIndex(fromPaneIndex)
+      if item = fromPane.itemAtIndex(fromItemIndex)
+        fromPane.removeItem(item)
+
+    @clearDropTarget()
+
+  clearDropTarget: ->
+    @find(".is-dragging").removeClass 'is-dragging'
+    @removeDropTargetClasses()
+    @removePlaceholderElement()
+
   onDrop: (event) =>
     event.preventDefault()
     event.stopPropagation()

     return unless event.originalEvent.dataTransfer.getData('atom-event') is 'true'

-    @find(".is-dragging").removeClass 'is-dragging'
-    @removeDropTargetClasses()
-    @removePlaceholderElement()
-
-    dataTransfer  = event.originalEvent.dataTransfer
-    fromIndex     = parseInt(dataTransfer.getData('sortable-index'))
-    fromPaneIndex = parseInt(dataTransfer.getData('from-pane-index'))
-    fromPane      = @paneContainer.paneAtIndex(fromPaneIndex)
-    toIndex       = @getDropTargetIndex(event)
-    toPane        = $(event.target).closest('.pane').view()
-    draggedTab    = fromPane.find(".tab-bar .sortable:eq(#{fromIndex})").view()
-    item          = draggedTab.item
-
-    if toPane is fromPane
-      toIndex-- if fromIndex < toIndex
-      toPane.moveItem(item, toIndex)
-    else
-      fromPane.moveItemToPane(item, toPane, toIndex--)
-    toPane.showItem(item)
-    toPane.focus()
+    fromProcessId = parseInt(event.originalEvent.dataTransfer.getData('from-process-id'))
+    fromRoutingId = parseInt(event.originalEvent.dataTransfer.getData('from-routing-id'))
+    fromIndex = parseInt(event.originalEvent.dataTransfer.getData('sortable-index'))
+    fromPaneIndex = parseInt(event.originalEvent.dataTransfer.getData('from-pane-index'))
+
+    @clearDropTarget()
+    if fromProcessId is @getProcessId()
+      fromPane = @paneContainer.paneAtIndex(fromPaneIndex)
+      toIndex = @getDropTargetIndex(event)
+      toPane = $(event.target).closest('.pane').view()
+      {item} = fromPane.find(".tab-bar .sortable:eq(#{fromIndex})").view() ? {}
+
+      if item?
+        if toPane is fromPane
+          toIndex-- if fromIndex < toIndex
+          toPane.moveItem(item, toIndex)
+        else
+          fromPane.moveItemToPane(item, toPane, toIndex--)
+        toPane.showItem(item)
+        toPane.focus()
+    else
+      if droppedPath = event.originalEvent.dataTransfer.getData('text/plain')
+        atom.workspace.open(droppedPath)
+        atom.focus()
+        BrowserIpc.sendChannel(fromProcessId, fromRoutingId, 'tab:dropped', fromIndex, fromPaneIndex)

   removeDropTargetClasses: ->
     atom.workspaceView.find('.tab-bar .is-drop-target').removeClass 'is-drop-target'
@@ -228,11 +257,10 @@
       sortables.index(el) + 1

   getPlaceholderElement: ->
-    @placeholderEl = $('<li/>', class: 'placeholder') unless @placeholderEl
-    @placeholderEl
+    @placeholderEl ?= $('<li/>', class: 'placeholder')

   removePlaceholderElement: ->
-    @placeholderEl.remove() if @placeholderEl
+    @placeholderEl?.remove()
     @placeholderEl = null

   isPlaceholderElement: (element) ->


Tidying (albeit somewhat subjective tidying) of bigint parsing.

--- lib/token/bigint.coffee
+++ lib/token/bigint.coffee
@@ -1,50 +1,49 @@
-array_is_zeroed = (array) ->
-  array[0] is 0 and array[1] is 0 and array[2] is 0 and array[3] is 0 and array[4] is 0 and array[5] is 0 and array[6] is 0 and array[7] is 0
+isZero = (array) ->
+  for byte in array
+    if byte != 0
+      return false

-getNextRemainder = (x) ->
+  true
+
+getNextRemainder = (array) ->
   remainder = 0
-  s = undefined
-  i = 7

-  while i >= 0
-    s = (remainder * 256) + x[i]
-    x[i] = Math.floor(s / 10)
+  for index in [array.length - 1..0] by -1
+    s = (remainder * 256) + array[index]
+    array[index] = Math.floor(s / 10)
     remainder = s % 10
-    i--
-  "" + remainder
+
+  remainder

 invert = (array) ->
-  i = 7
+  # Invert bits
+  for byte, index in array
+    array[index] = array[index] ^ 0xFF

-  while i >= 0
-    array[i] = array[i] ^ 0xFF
-    i--
-  i = 0
-
-  while i < 8
-    array[i] = array[i] + 1
-    if array[i] > 255
-      array[i] = 0
+  for byte, index in array
+    array[index] = array[index] + 1
+    if array[index] > 255
+      array[index] = 0
     else
       break
-    i++

 convertLEBytesToString = (buffer) ->
-  array = []
-  i = 0
+  array = Array.prototype.slice.call(buffer, 0, buffer.length)

-  while i <= 7
-    array[i] = buffer[i]
-    i++
-  if array_is_zeroed(array)
-    "0"
+  if isZero(array)
+    '0'
   else
-    sign = (if (array[7] & 0x80) is 0x80 then "-" else "")
-    invert array  if sign is "-"
-    result = ""
-    until array_is_zeroed(array)
+    if array[array.length - 1] & 0x80
+      sign = '-'
+      invert(array)
+    else
+      sign = ''
+
+    result = ''
+    until isZero(array)
       t = getNextRemainder(array)
-      result = t + "" + result
+      result = t + result
+
     sign + result

-module.exports.convertLEBytesToString = convertLEBytesToString+module.exports.convertLEBytesToString = convertLEBytesToString


:bug: Delay the package activation to when all packages are active

--- lib/minimap-find-and-replace.coffee
+++ lib/minimap-find-and-replace.coffee
@@ -2,16 +2,20 @@

 module.exports =
   binding: null
+
   activate: (state) ->
-    findPackage = atom.packages.getLoadedPackage('find-and-replace')
-    minimapPackage = atom.packages.getLoadedPackage('minimap')
+    disposable = atom.packages.onDidActivateAll =>
+      disposable.dispose()

-    return @deactivate() unless findPackage? and minimapPackage?
+      findPackage = atom.packages.getLoadedPackage('find-and-replace')
+      minimapPackage = atom.packages.getLoadedPackage('minimap')

-    minimap = require(minimapPackage.mainModulePath or minimapPackage.path)
-    return @deactivate() unless minimap.versionMatch('3.x')
+      return @deactivate() unless findPackage? and minimapPackage?

-    @binding = new MinimapFindAndReplaceBinding findPackage, minimapPackage
+      minimap = require(minimapPackage.mainModulePath or minimapPackage.path)
+      return @deactivate() unless minimap.versionMatch('3.x')
+
+      @binding = new MinimapFindAndReplaceBinding findPackage, minimapPackage

   deactivate: ->
     @binding?.deactivate()


Change selected personalisation order

Change selected personalisation order

--- src/views/personalisation.coffee
+++ src/views/personalisation.coffee
@@ -21,12 +21,6 @@
             'click .personalisation-message .close-button': 'closeMessage'

         personalisationIcons:
-            'city': [
-                'helsinki'
-                'espoo'
-                'vantaa'
-                'kauniainen'
-            ]
             'senses': [
                 'hearing_aid'
                 'visually_impaired'
@@ -37,6 +31,12 @@
                 'reduced_mobility'
                 'rollator'
                 'stroller'
+            ]
+            'city': [
+                'helsinki'
+                'espoo'
+                'vantaa'
+                'kauniainen'
             ]

         initialize: ->


changing the space invisible to 00b7 'middot'

--- src/app/editor.coffee
+++ src/app/editor.coffee
@@ -312,7 +312,7 @@
   setInvisibles: (@invisibles={}) ->
     _.defaults @invisibles,
       eol: '\u00ac'
-      space: '\u2022'
+      space: '\u00b7'
       tab: '\u00bb'
       cr: '\u00a4'
     @resetDisplay()