sess fix

sess fix

--- lib/Vein.coffee
+++ lib/Vein.coffee
@@ -11,8 +11,9 @@

     @server = sockjs.createServer @opts
     @server.on 'connection', (socket) =>
-      socket.cookie = socket._session.recv.ws.request.headers.cookie
-      socket.session = parseCookie(socket.cookie)[@opts.sessionName] if socket.cookie?
+      try
+        socket.cookie = socket._session.recv.ws.request.headers.cookie
+        socket.session = parseCookie(socket.cookie)[@opts.sessionName] if socket.cookie?

       socket.write JSON.stringify id: 'services', args: Object.keys @routes # send down list of services
       socket.on 'data', (msg) => @route socket, msg
@@ -42,7 +43,7 @@
     send = (args...) -> write socket, args...
     unless @opts.noTrack
       send.all = (args...) => write sock, args... for id, sock of @clients
-    send.session = (sess) =>
+    send.session = (sess) =>
       socket.session = sess
       socket.write JSON.stringify id: 'session', args: [sess]
     @routes[service] send, socket, args...


progress toward list editing UI

progress toward list editing UI

--- @client/lists.coffee
+++ @client/lists.coffee
@@ -1,12 +1,12 @@
 require './questionaire'

-# window.Cluster is temporary for hala.consider.it. After deployed, just need to change it in Hala's customization
-window.List = window.Cluster = ReactiveComponent
+window.List = ReactiveComponent
   displayName: 'List'

   # list of proposals
   render: ->
+
     list = @props.list

     current_user = fetch '/current_user'
@@ -18,11 +18,11 @@
     collapsed = fetch 'collapsed_lists'
     is_collapsed = !!collapsed[@props.key]

-    proposals = sorted_proposals(list.proposals, @local.key, true)
-
-    return SPAN null if !proposals
+    proposals = if !@props.fresh then sorted_proposals(list.proposals, @local.key, true) or [] else []

     list_key = "list/#{list.name}"
+
+    edit_list = fetch "edit-#{list_key}"

     ARTICLE
       key: list.name
@@ -34,25 +34,26 @@
       A name: if list.name && list.name then list.name.toLowerCase().replace(/ /g, '_')

-      ManualProposalResort sort_key: @local.key
-
-      ListHeading
+      if !@props.fresh
+        ManualProposalResort sort_key: @local.key
+
+      ListHeader
         list: list
         proposals_count: proposals.length
+        fresh: @props.fresh

       if customization('questionaire', list_key) && !is_collapsed
         Questionaire
           list_key: list_key

-      else if !is_collapsed
+      else if !is_collapsed && !@props.fresh
         UL null,
           for proposal,idx in proposals
-
             CollapsedProposal
               key: "collapsed#{proposal.key}"
               proposal: proposal

-          if customization('list_show_new_button', list_key)
+          if customization('list_show_new_button', list_key) && !edit_list.editing
             LI
               key: "new#{list_key}"
               style:
@@ -72,229 +73,29 @@
       if customization('footer', list_key) && !is_collapsed
         customization('footer', list_key)()

-
-GearIcon = (opts) ->
-  SVG
-    height: opts.size or '100px'
-    width: opts.size or '100px'
-    fill: opts.fill or "#888"
-    x: "0px"
-    y: "0px"
-    viewBox: "0 0 100 100"
-    dangerouslySetInnerHTML: __html: '<path d="M95.784,59.057c1.867,0,3.604-1.514,3.858-3.364c0,0,0.357-2.6,0.357-5.692c0-3.092-0.357-5.692-0.357-5.692  c-0.255-1.851-1.991-3.364-3.858-3.364h-9.648c-1.868,0-3.808-1.191-4.31-2.646s-1.193-6.123,0.128-7.443l6.82-6.82  c1.32-1.321,1.422-3.575,0.226-5.01L80.976,11c-1.435-1.197-3.688-1.095-5.01,0.226l-6.82,6.82c-1.32,1.321-3.521,1.853-4.888,1.183  c-1.368-0.67-5.201-3.496-5.201-5.364V4.217c0-1.868-1.514-3.604-3.364-3.859c0,0-2.6-0.358-5.692-0.358s-5.692,0.358-5.692,0.358  c-1.851,0.254-3.365,1.991-3.365,3.859v9.648c0,1.868-1.19,3.807-2.646,4.31c-1.456,0.502-6.123,1.193-7.444-0.128l-6.82-6.82  C22.713,9.906,20.459,9.804,19.025,11L11,19.025c-1.197,1.435-1.095,3.689,0.226,5.01l6.819,6.82  c1.321,1.321,1.854,3.521,1.183,4.888s-3.496,5.201-5.364,5.201H4.217c-1.868,0-3.604,1.514-3.859,3.364c0,0-0.358,2.6-0.358,5.692  c0,3.093,0.358,5.692,0.358,5.692c0.254,1.851,1.991,3.364,3.859,3.364h9.648c1.868,0,3.807,1.19,4.309,2.646  c0.502,1.455,1.193,6.122-0.128,7.443l-6.819,6.819c-1.321,1.321-1.423,3.575-0.226,5.01L19.025,89  c1.435,1.196,3.688,1.095,5.009-0.226l6.82-6.82c1.321-1.32,3.521-1.853,4.889-1.183c1.368,0.67,5.201,3.496,5.201,5.364v9.648  c0,1.867,1.514,3.604,3.365,3.858c0,0,2.599,0.357,5.692,0.357s5.692-0.357,5.692-0.357c1.851-0.255,3.364-1.991,3.364-3.858v-9.648  c0-1.868,1.19-3.808,2.646-4.31s6.123-1.192,7.444,0.128l6.819,6.82c1.321,1.32,3.575,1.422,5.01,0.226L89,80.976  c1.196-1.435,1.095-3.688-0.227-5.01l-6.819-6.819c-1.321-1.321-1.854-3.521-1.183-4.889c0.67-1.368,3.496-5.201,5.364-5.201H95.784  z M50,68.302c-10.108,0-18.302-8.193-18.302-18.302c0-10.107,8.194-18.302,18.302-18.302c10.108,0,18.302,8.194,18.302,18.302  C68.302,60.108,60.108,68.302,50,68.302z"></path>'
-
-EditableTitle = ReactiveComponent
-  displayName: 'EditableTitle'
-
-  render: ->
-    subdomain = fetch '/subdomain'
-    current_user = fetch '/current_user'
-
+EditList = ReactiveComponent
+  displayName: 'EditList'
+
+  render: ->
     list = @props.list
     list_key = "list/#{list.name}"

-    edit_list = fetch "edit-#{list_key}"
-
-    list_items_title = customization('list_items_title', list_key) or list.name or 'Proposals'
-
-    title = (edit_list.editing and edit_list.list_title) or customization('list_title', list_key) or list_items_title
-
-    if title == 'Show all'
-      title = translator "engage.all_proposals_list", "All Proposals"
-    else if title == "Proposals"
-      title = translator "engage.default_proposals_list", "Proposals"
-    else
-      title = translator
-                id: "proposal_list.#{title}"
-                key: "/translations/#{subdomain.name}"
-                title
-
-    heading_style = _.defaults {}, customization('list_label_style', list_key),
-      fontSize: 36
-      fontWeight: 700
-      fontStyle: 'oblique'
-
-    if title.replace(/^\s+|\s+$/g, '').length == 0 # trim whitespace
-      heading_style.fontSize = 0
-
-    list_uncollapseable = customization 'list_uncollapseable', list_key
-    TITLE_WRAPPER = if list_uncollapseable then DIV else BUTTON
-
-    collapsed = fetch 'collapsed_lists'
-    is_collapsed = !!collapsed[list_key]
-
-    tw = if is_collapsed then 15 else 20
-    th = if is_collapsed then 20 else 15
-
-    toggle_list = ->
-      if !list_uncollapseable
-        collapsed[list_key] = !collapsed[list_key]
-        save collapsed
-
-    is_admin = fetch('/current_user').is_admin
-    title_style =
-      padding: 0
-      margin: 0
-      border: 'none'
-      backgroundColor: 'transparent'
-      textAlign: 'left'
-      color: heading_style.color
-      fontWeight: heading_style.fontWeight
-      fontFamily: heading_style.fontFamily
-      fontStyle: heading_style.fontStyle
-      textDecoration: heading_style.textDecoration
-
-    H1
-      style: heading_style
-
-      if edit_list.editing
-        AutoGrowTextArea
-          id: "title-#{list_key}"
-          ref: 'input'
-          style: _.extend {}, title_style,
-            fontSize: heading_style.fontSize
-            width: '100%'
-            lineHeight: 1.4
-
-          defaultValue: title
-          onChange: (e) ->
-            edit_list.list_title = e.target.value
-            save edit_list
-          placeholder: translator('engage.list_title_input-open.placeholder', 'Ask an open-ended question or set a list title')
-
-
-      else
-        TITLE_WRAPPER
-          tabIndex: if !list_uncollapseable then 0
-          'aria-label': "#{title}. #{translator('Expand or collapse list.')}"
-          'aria-pressed': !collapsed[list_key]
-          onMouseEnter: => @local.hover_label = true; save @local
-          onMouseLeave: => @local.hover_label = false; save @local
-          style: _.extend {}, title_style,
-            cursor: if !list_uncollapseable then 'pointer'
-            position: 'relative'
-
-          onKeyDown: if !list_uncollapseable then (e) ->
-            if e.which == 13 || e.which == 32 # ENTER or SPACE
-              toggle_list()
-              e.preventDefault()
-          onClick: if !list_uncollapseable then (e) ->
-            toggle_list()
-            document.activeElement.blur()
-
-          title
-
-          if current_user.is_admin
-            SPAN
-              style:
-                position: 'absolute'
-                right: -28
-
-              onKeyDown: (e) =>
-                if e.which == 13 || e.which == 32 # ENTER or SPACE
-                  e.target.click()
-                  e.preventDefault()
-
-              onClick: if is_admin then (e) =>
-                edit_list.editing = true
-                save edit_list
-                e.preventDefault()
-                e.stopPropagation()
-
-                setTimeout =>
-                  @refs.input?.getDOMNode().focus()
-                  @refs.input?.getDOMNode().setSelectionRange(-1, -1) # put cursor at end
-
-              GearIcon
-                size: 20
-                fill: '#888'
-
-
-          if !list_uncollapseable
-            SPAN
-              'aria-hidden': true
-              style: cssTriangle (if is_collapsed then 'right' else 'bottom'), (heading_style.color or 'black'), tw, th,
-                position: 'absolute'
-                left: -tw - 20
-                top: 16
-                width: tw
-                height: th
-                display: if @local.hover_label or is_collapsed then 'inline-block' else 'none'
-                outline: 'none'
-
-
-EditableDescription = ReactiveComponent
-  displayName: 'EditableDescription'
-  render: ->
-    list = @props.list
-
-    list_key = "list/#{list.name}"
-    edit_list = fetch "edit-#{list_key}"
-
-    description = edit_list.list_description or customization('list_description', list_key)
-    description_style = customization 'list_description_style', list_key
-
-    current_user = fetch '/current_user'
-
-    DIV
-      style: _.defaults {}, (description_style or {}),
-        fontSize: 18
-        fontWeight: 400
-        color: '#444'
-        marginTop: 6
-
-      if _.isFunction description
-        description()
-      else
-
-        if current_user.is_admin && edit_list.editing
-          DIV
-            id: 'edit_description'
-            STYLE
-              dangerouslySetInnerHTML: __html: """
-                #edit_description .ql-editor {
-                  min-height: 48px;
-                }
-              """
-
-            WysiwygEditor
-              key: "#{list_key}-description"
-              horizontal: true
-              html: customization('list_description', list_key)
-              placeholder: translator("engage.list_description", "(optional) Description")
-
-        else
-          desc = description
-          if typeof desc == 'string'
-            desc = [description]
-
-          for para, idx in desc
-            DIV
-              key: idx
-              style:
-                marginBottom: 10
-              dangerouslySetInnerHTML: {__html: para}
-
-
-EditList = ReactiveComponent
-  displayName: 'EditList'
-
-  render: ->
-    list = @props.list
-    list_key = "list/#{list.name}"
-
     current_user = fetch '/current_user'
     edit_list = fetch "edit-#{list_key}"
-
-    return SPAN null if !current_user.is_admin || !edit_list.editing
+    subdomain = fetch '/subdomain'
+
+    if @props.fresh && !edit_list.editing
+      edit_list.editing = true
+
+    return SPAN null if !current_user.is_admin

     submit = =>
-      subdomain = fetch '/subdomain'
       customizations = JSON.parse subdomain.customizations
+
       customizations[list_key] ?= {}
       list_config = customizations[list_key]

-      fields = ['list_title', 'list_description', 'list_show_new_button']
+      fields = ['list_title', 'list_description', 'list_show_new_button', 'slider_pole_labels']

       for f in fields
         val = edit_list[f]
@@ -304,12 +105,27 @@
       description = fetch("#{list_key}-description").html
       list_config.list_description = description

+      if @props.fresh
+        new_key = "list/#{slugify(list.title)}-#{Math.round(Math.random() * 100)}"
+        customizations[new_key] = customizations[list_key]
+        delete customizations[list_key]
+
       subdomain.customizations = JSON.stringify customizations, null, 2

       save subdomain, =>
         if subdomain.errors
           console.error "Failed to save list changes", subdomain.errors

+        exit_edit()
+
+    cancel_edit = =>
+      customizations = JSON.parse subdomain.customizations
+
+      if @props.fresh && list_key of customizations
+        delete customizations[list_key]
+        subdomain.customizations = JSON.stringify customizations, null, 2
+        save subdomain
+      else
         exit_edit()

     exit_edit = =>
@@ -320,44 +136,107 @@

       save edit_list

-
-    DIV
-      style:
-        position: 'absolute'
-        top: -35
-        left: 0
-
-      BUTTON
+    admin_actions = [{action: 'edit', label: t('edit')}, {action: 'delete', label: t('delete')}]
+
+    if !edit_list.editing
+
+      DropMenu
+        options: admin_actions
+        open_menu_on: 'activation'
+
+        render_anchor: ->
+          GearIcon
+            size: 20
+            fill: '#888'
+
+        render_option: (option, is_active) ->
+          SPAN null,
+            option.label
+
+
+        selection_made_callback: (option) =>
+          if option.action == 'edit'
+            edit_list.editing = true
+            save edit_list
+
+            setTimeout =>
+              @refs.input?.getDOMNode().focus()
+              @refs.input?.getDOMNode().setSelectionRange(-1, -1) # put cursor at end
+          else if option.action == 'delete'
+            # TODO: what to do if there are proposals?
+            customizations = JSON.parse subdomain.customizations
+            delete customizations[list_key]
+            subdomain.customizations = JSON.stringify customizations, null, 2
+            save subdomain
+
+
+        wrapper_style:
+          position: 'absolute'
+          right: -28
+          top: 16
+
+        anchor_style: {}
+
+        menu_style:
+          backgroundColor: '#eee'
+          border: "1px solid #{focus_color()}"
+          right: -9999
+          top: 18
+          borderRadius: 8
+          fontWeight: 400
+          overflow: 'hidden'
+          boxShadow: '0 1px 2px rgba(0,0,0,.3)'
+          fontSize: 18
+          fontStyle: 'normal'
+
+        menu_when_open_style:
+          right: 0
+
+        option_style:
+          padding: '6px 12px'
+          borderBottom: "1px solid #ddd"
+          display: 'block'
+
+        active_option_style:
+          color: 'white'
+          backgroundColor: focus_color()
+
+    else
+
+      DIV
         style:
-          border: 'none'
-          borderRadius: 8
-          padding: '4px 8px'
-        onClick: submit
-        onKeyDown: (e) =>
-          if e.which == 13 || e.which == 32 # ENTER or SPACE
-            @submit(e)
-            e.preventDefault()
-
-        translator 'engage.save_changes_button', 'Save'
-
-      BUTTON
-        style:
-          backgroundColor: 'transparent'
-          border: 'none'
-          textDecoration: 'underline'
-        onClick: exit_edit
-        onKeyDown: (e) =>
-          if e.which == 13 || e.which == 32 # ENTER or SPACE
-            @exit_edit(e)
-            e.preventDefault()
-
-        translator 'engage.cancel_button', 'cancel'
-
-
-
-
-window.ListHeading = ReactiveComponent
-  displayName: 'ListHeading'
+          position: 'absolute'
+          top: -35
+          left: 0
+
+        BUTTON
+          style:
+            border: 'none'
+            borderRadius: 8
+            padding: '4px 8px'
+          onClick: submit
+          onKeyDown: (e) =>
+            if e.which == 13 || e.which == 32 # ENTER or SPACE
+              @submit(e)
+              e.preventDefault()
+
+          translator 'engage.save_changes_button', 'Save'
+
+        BUTTON
+          style:
+            backgroundColor: 'transparent'
+            border: 'none'
+            textDecoration: 'underline'
+          onClick: cancel_edit
+          onKeyDown: (e) =>
+            if e.which == 13 || e.which == 32 # ENTER or SPACE
+              cancel_edit(e)
+              e.preventDefault()
+
+          translator 'engage.cancel_button', 'cancel'
+
+window.ListHeader = ReactiveComponent
+  displayName: 'ListHeader'

   render: ->
     list = @props.list
@@ -388,6 +267,7 @@

         EditList
           list: @props.list
+          fresh: @props.fresh

         EditableTitle
           list: @props.list
@@ -427,6 +307,277 @@
                 key: if histo_title == customizations.default.list_opinions_title then '/translations' else "/translations/#{subdomain.name}"
                 histo_title

+        if edit_list.editing
+          DIV null,
+            DIV
+              style:
+                padding: '12px 0'
+
+              LABEL
+                style:
+                  marginRight: 4
+                htmlFor: 'list_show_new_button'
+
+                translator "engage.list-config-who-can-add", "Who can add responses to this list?"
+
+              DIV null,
+                INPUT
+                  id: 'any-participant'
+                  type: 'radio'
+                  name: 'list_show_new_button'
+                  defaultChecked: customization('list_show_new_button', list_key)
+                  onChange: (e) =>
+                    edit_list.list_show_new_button = true
+                    save edit_list
+
+                LABEL
+                  style:
+                    marginRight: 4
+                  htmlFor: 'any-participant'
+
+                  translator "engage.list-config-who-can-add-anyone", "Any registered participant"
+
+              DIV null,
+                INPUT
+                  id: 'host-only'
+                  type: 'radio'
+                  name: 'list_show_new_button'
+                  defaultChecked: !customization('list_show_new_button', list_key)
+                  onChange: (e) =>
+                    edit_list.list_show_new_button = false
+                    save edit_list
+
+                LABEL
+                  style:
+                    marginRight: 4
+                  htmlFor: 'host-only'
+
+                  translator "engage.list-config-who-can-add-only-hosts", "Only forum hosts"
+
+            DIV
+              style:
+                padding: '12px 0'
+
+              LABEL
+                style: {}
+
+                translator "engage.list-config-spectrum", "On what spectrum is each response evaluated?"
+
+
+
+              DIV
+                ref: 'slider_config'
+                style:
+                  margin: '12px 48px'
+                  position: 'relative'
+                  width: column_sizes().second
+
+                SPAN
+                  style:
+                    display: 'block'
+                    width: '100%'
+                    borderBottom: '1px solid'
+                    borderColor: '#999'
+
+                INPUT
+                  type: 'text'
+                  style:
+                    position: 'relative'
+                    left: 0
+                    border: '1px solid'
+                    borderColor: if edit_list.slider_pole_labels && edit_list.slider_pole_labels.oppose == '' then '#ccc' else 'transparent'
+                    outline: 'none'
+                    color: '#999'
+                  ref: 'oppose_slider'
+                  defaultValue: customization('slider_pole_labels', list_key).oppose
+                  onChange: (e) ->
+                    edit_list.slider_pole_labels ?= {}
+                    edit_list.slider_pole_labels.oppose = e.target.value
+                    save edit_list
+
+                INPUT
+                  type: 'text'
+                  style:
+                    position: 'absolute'
+                    right: 0
+                    border: '1px solid'
+                    borderColor: if edit_list.slider_pole_labels && edit_list.slider_pole_labels.support == '' then '#ccc' else 'transparent'
+                    outline: 'none'
+                    textAlign: 'right'
+                    color: '#999'
+
+                  ref: 'support_slider'
+                  defaultValue: customization('slider_pole_labels', list_key).support
+                  onChange: (e) ->
+                    edit_list.slider_pole_labels ?= {}
+                    edit_list.slider_pole_labels.support = e.target.value
+                    save edit_list
+
+
+
+              DropMenu
+                options: (v for k,v of slider_labels).concat({support: '', oppose: ''})
+                open_menu_on: 'activation'
+                selection_made_callback: (option) =>
+                  @refs.oppose_slider.getDOMNode().value = option.oppose
+                  @refs.support_slider.getDOMNode().value = option.support
+                  edit_list.slider_pole_labels =
+                    support: option.support
+                    oppose: option.oppose
+                  save edit_list
+
+                  setTimeout =>
+                    $(@refs.slider_config.getDOMNode()).ensureInView()
+                  , 0
+
+
+                render_anchor: ->
+
+                  DIV
+                    ref: 'slider_config'
+                    style:
+                      padding: '18px 48px 32px 48px'
+                      position: 'relative'
+                      width: column_sizes().second + 48 * 2
+
+
+                    DIV
+                      style:
+                        position: 'relative'
+
+                      SPAN
+                        style:
+                          display: 'block'
+                          width: '100%'
+                          borderBottom: '1px solid'
+                          borderColor: '#999'
+
+
+
+                      INPUT
+                        type: 'text'
+                        style:
+                          position: 'absolute'
+                          left: 0
+                          border: '1px solid'
+                          borderColor: if edit_list.slider_pole_labels && edit_list.slider_pole_labels.oppose == '' then '#ccc' else 'transparent'
+                          outline: 'none'
+                          color: '#999'
+                          fontSize: 16
+                        ref: 'oppose_slider'
+                        defaultValue: customization('slider_pole_labels', list_key).oppose
+                        onChange: (e) ->
+                          edit_list.slider_pole_labels ?= {}
+                          edit_list.slider_pole_labels.oppose = e.target.value
+                          save edit_list
+
+                      INPUT
+                        type: 'text'
+                        style:
+                          position: 'absolute'
+                          right: 0
+                          border: '1px solid'
+                          borderColor: if edit_list.slider_pole_labels && edit_list.slider_pole_labels.support == '' then '#ccc' else 'transparent'
+                          outline: 'none'
+                          color: '#999'
+                          fontSize: 16
+                          textAlign: 'right'
+
+                        ref: 'support_slider'
+                        defaultValue: customization('slider_pole_labels', list_key).support
+                        onChange: (e) ->
+                          edit_list.slider_pole_labels ?= {}
+                          edit_list.slider_pole_labels.support = e.target.value
+                          save edit_list
+
+                    SPAN
+                      style:
+                        fontWeight: 700
+                        position: 'absolute'
+                        right: 8
+                        top: 13
+
+                      SPAN style: _.extend cssTriangle 'bottom', focus_color(), 15, 9,
+                        display: 'inline-block'
+                        marginLeft: 4
+                        marginBottom: 2
+
+                render_option: (option, is_active) ->
+                  if option.oppose == ''
+                    return  DIV
+                              style:
+                                fontSize: 22
+
+                              'Custom'
+
+                  DIV
+                    style:
+                      margin: '12px 48px'
+                      position: 'relative'
+                      fontSize: 16
+
+                    SPAN
+                      style:
+                        display: 'inline-block'
+                        width: '100%'
+                        borderBottom: '1px solid'
+                        borderColor: if is_active then 'white' else '#999'
+
+                    BR null
+                    SPAN
+                      style:
+                        position: 'relative'
+                        left: 0
+
+                      option.oppose
+
+                    SPAN
+                      style:
+                        position: 'absolute'
+                        right: 0
+                      option.support
+
+                # wrapper_style:
+                #   display: 'inline-block'
+                #   position: 'absolute'
+                #   right: -38
+
+                anchor_style:
+                  fontWeight: 600
+                  padding: 0
+                  # display: 'inline-block'
+                  color: focus_color() #'inherit'
+                  textTransform: 'lowercase'
+                  borderRadius: 16
+                  border: "1px solid #{focus_color()}"
+
+                menu_style:
+                  width: column_sizes().second + 48 * 2
+                  backgroundColor: '#eee'
+                  border: "1px solid #{focus_color()}"
+                  left: -9999
+                  top: 51
+                  borderRadius: 8
+                  fontWeight: 400
+                  overflow: 'hidden'
+                  boxShadow: '0 1px 2px rgba(0,0,0,.3)'
+
+                menu_when_open_style:
+                  left: 0
+
+                option_style:
+                  padding: '6px 0px'
+                  # borderBottom: "1px solid #ddd"
+                  display: 'block'
+
+                active_option_style:
+                  color: 'white'
+                  backgroundColor: focus_color()
+
+
+
+
+

       if @props.proposals_count > 0 && !customization('questionaire', list_key) && !is_collapsed && !customization('list_no_filters', list_key)
         list_actions
@@ -434,8 +585,226 @@
           add_new: customization('list_show_new_button', list_key) && !is_collapsed && @props.proposals_count > 4
           can_sort: customization('homepage_show_search_and_sort') && @props.proposals_count > 8

-
-
+window.NewList = ReactiveComponent
+  displayName: 'NewList'
+
+  render: ->
+    subdomain = fetch '/subdomain'
+
+    if !@local.edit_key
+      @local.edit_key = "new-list-#{Math.round(Math.random() * 1000)}"
+
+    list =
+      name: @local.edit_key
+
+    list_key = "list/#{list.name}"
+    edit_list = fetch "edit-#{list_key}"
+
+    if edit_list.editing
+      List
+        fresh: true
+        list: list
+
+
+
+    else
+      BUTTON
+        style:
+          textAlign: 'left'
+          marginTop: 35
+          display: 'block'
+          padding: '18px 24px'
+          position: 'relative'
+          left: -24
+          border: 'none'
+          width: '100%'
+          borderRadius: 8
+
+        onClick: =>
+          edit_list.editing = true
+          save edit_list
+
+        onKeyDown: (e) =>
+          if e.which == 13 || e.which == 32 # ENTER or SPACE
+            e.target.click()
+            e.preventDefault()
+
+        H1
+          style:
+            fontSize: 36
+            fontWeight: 700
+            fontStyle: 'oblique'
+            color: '#888'
+          translator 'engage.create_new_list_button', "Create new list"
+
+
+EditableTitle = ReactiveComponent
+  displayName: 'EditableTitle'
+
+  render: ->
+    subdomain = fetch '/subdomain'
+    current_user = fetch '/current_user'
+
+    list = @props.list
+    list_key = "list/#{list.name}"
+
+    edit_list = fetch "edit-#{list_key}"
+
+    list_items_title = customization('list_items_title', list_key) or list.name or 'Proposals'
+
+    title = (edit_list.editing and edit_list.list_title) or customization('list_title', list_key) or list_items_title
+
+    if title == 'Show all'
+      title = translator "engage.all_proposals_list", "All Proposals"
+    else if title == "Proposals"
+      title = translator "engage.default_proposals_list", "Proposals"
+    else
+      title = translator
+                id: "proposal_list.#{title}"
+                key: "/translations/#{subdomain.name}"
+                title
+
+    heading_style = _.defaults {}, customization('list_label_style', list_key),
+      fontSize: 36
+      fontWeight: 700
+      fontStyle: 'oblique'
+
+    if title.replace(/^\s+|\s+$/g, '').length == 0 # trim whitespace
+      heading_style.fontSize = 0
+
+    list_uncollapseable = customization 'list_uncollapseable', list_key
+    TITLE_WRAPPER = if list_uncollapseable then DIV else BUTTON
+
+    collapsed = fetch 'collapsed_lists'
+    is_collapsed = !!collapsed[list_key]
+
+    tw = if is_collapsed then 15 else 20
+    th = if is_collapsed then 20 else 15
+
+    toggle_list = ->
+      if !list_uncollapseable
+        collapsed[list_key] = !collapsed[list_key]
+        save collapsed
+
+    is_admin = fetch('/current_user').is_admin
+    title_style =
+      padding: 0
+      margin: 0
+      border: 'none'
+      backgroundColor: 'transparent'
+      textAlign: 'left'
+      color: heading_style.color
+      fontWeight: heading_style.fontWeight
+      fontFamily: heading_style.fontFamily
+      fontStyle: heading_style.fontStyle
+      textDecoration: heading_style.textDecoration
+
+    H1
+      style: heading_style
+
+      if edit_list.editing
+        AutoGrowTextArea
+          id: "title-#{list_key}"
+          ref: 'input'
+          style: _.extend {}, title_style,
+            fontSize: heading_style.fontSize
+            width: '100%'
+            lineHeight: 1.4
+
+          defaultValue: title
+          onChange: (e) ->
+            edit_list.list_title = e.target.value
+            save edit_list
+          placeholder: translator('engage.list_title_input-open.placeholder', 'Ask an open-ended question or set a list title')
+
+
+      else
+
+        TITLE_WRAPPER
+          tabIndex: if !list_uncollapseable then 0
+          'aria-label': "#{title}. #{translator('Expand or collapse list.')}"
+          'aria-pressed': !collapsed[list_key]
+          onMouseEnter: => @local.hover_label = true; save @local
+          onMouseLeave: => @local.hover_label = false; save @local
+          style: _.extend {}, title_style,
+            cursor: if !list_uncollapseable then 'pointer'
+            position: 'relative'
+
+          onKeyDown: if !list_uncollapseable then (e) ->
+            if e.which == 13 || e.which == 32 # ENTER or SPACE
+              toggle_list()
+              e.preventDefault()
+          onClick: if !list_uncollapseable then (e) ->
+            toggle_list()
+            document.activeElement.blur()
+
+          title
+
+          if !list_uncollapseable
+            SPAN
+              'aria-hidden': true
+              style: cssTriangle (if is_collapsed then 'right' else 'bottom'), (heading_style.color or 'black'), tw, th,
+                position: 'absolute'
+                left: -tw - 20
+                top: 16
+                width: tw
+                height: th
+                display: if @local.hover_label or is_collapsed then 'inline-block' else 'none'
+                outline: 'none'
+
+
+
+EditableDescription = ReactiveComponent
+  displayName: 'EditableDescription'
+  render: ->
+    list = @props.list
+
+    list_key = "list/#{list.name}"
+    edit_list = fetch "edit-#{list_key}"
+
+    description = edit_list.list_description or customization('list_description', list_key)
+    description_style = customization 'list_description_style', list_key
+
+    current_user = fetch '/current_user'
+
+    DIV
+      style: _.defaults {}, (description_style or {}),
+        fontSize: 18
+        fontWeight: 400
+        color: '#444'
+        marginTop: 6
+
+      if _.isFunction description
+        description()
+      else
+
+        if current_user.is_admin && edit_list.editing
+          DIV
+            id: 'edit_description'
+            STYLE
+              dangerouslySetInnerHTML: __html: """
+                #edit_description .ql-editor {
+                  min-height: 48px;
+                }
+              """
+
+            WysiwygEditor
+              key: "#{list_key}-description"
+              horizontal: true
+              html: customization('list_description', list_key)
+              placeholder: translator("engage.list_description", "(optional) Description")
+
+        else
+          desc = description
+          if typeof desc == 'string'
+            desc = [description]
+
+          for para, idx in desc
+            DIV
+              key: idx
+              style:
+                marginBottom: 10
+              dangerouslySetInnerHTML: {__html: para}

 window.list_actions = (props) ->
@@ -468,7 +837,8 @@
             e.stopPropagation()

             setTimeout =>
-              $("[name='add_new_#{props.cluster.name}']").ensureInView()
+              $("[name='add_new_#{props.list.name}']").ensureInView()
+              $("[name='add_new_#{props.list.name}']").click()
             , 1
           translator "engage.add_new_proposal_to_list", 'add new'

@@ -500,4 +870,16 @@

     DIV
       style:
-        clear: 'both'+        clear: 'both'
+
+
+GearIcon = (opts) ->
+  SVG
+    height: opts.size or '100px'
+    width: opts.size or '100px'
+    fill: opts.fill or "#888"
+    x: "0px"
+    y: "0px"
+    viewBox: "0 0 100 100"
+    dangerouslySetInnerHTML: __html: '<path d="M95.784,59.057c1.867,0,3.604-1.514,3.858-3.364c0,0,0.357-2.6,0.357-5.692c0-3.092-0.357-5.692-0.357-5.692  c-0.255-1.851-1.991-3.364-3.858-3.364h-9.648c-1.868,0-3.808-1.191-4.31-2.646s-1.193-6.123,0.128-7.443l6.82-6.82  c1.32-1.321,1.422-3.575,0.226-5.01L80.976,11c-1.435-1.197-3.688-1.095-5.01,0.226l-6.82,6.82c-1.32,1.321-3.521,1.853-4.888,1.183  c-1.368-0.67-5.201-3.496-5.201-5.364V4.217c0-1.868-1.514-3.604-3.364-3.859c0,0-2.6-0.358-5.692-0.358s-5.692,0.358-5.692,0.358  c-1.851,0.254-3.365,1.991-3.365,3.859v9.648c0,1.868-1.19,3.807-2.646,4.31c-1.456,0.502-6.123,1.193-7.444-0.128l-6.82-6.82  C22.713,9.906,20.459,9.804,19.025,11L11,19.025c-1.197,1.435-1.095,3.689,0.226,5.01l6.819,6.82  c1.321,1.321,1.854,3.521,1.183,4.888s-3.496,5.201-5.364,5.201H4.217c-1.868,0-3.604,1.514-3.859,3.364c0,0-0.358,2.6-0.358,5.692  c0,3.093,0.358,5.692,0.358,5.692c0.254,1.851,1.991,3.364,3.859,3.364h9.648c1.868,0,3.807,1.19,4.309,2.646  c0.502,1.455,1.193,6.122-0.128,7.443l-6.819,6.819c-1.321,1.321-1.423,3.575-0.226,5.01L19.025,89  c1.435,1.196,3.688,1.095,5.009-0.226l6.82-6.82c1.321-1.32,3.521-1.853,4.889-1.183c1.368,0.67,5.201,3.496,5.201,5.364v9.648  c0,1.867,1.514,3.604,3.365,3.858c0,0,2.599,0.357,5.692,0.357s5.692-0.357,5.692-0.357c1.851-0.255,3.364-1.991,3.364-3.858v-9.648  c0-1.868,1.19-3.808,2.646-4.31s6.123-1.192,7.444,0.128l6.819,6.82c1.321,1.32,3.575,1.422,5.01,0.226L89,80.976  c1.196-1.435,1.095-3.688-0.227-5.01l-6.819-6.819c-1.321-1.321-1.854-3.521-1.183-4.889c0.67-1.368,3.496-5.201,5.364-5.201H95.784  z M50,68.302c-10.108,0-18.302-8.193-18.302-18.302c0-10.107,8.194-18.302,18.302-18.302c10.108,0,18.302,8.194,18.302,18.302  C68.302,60.108,60.108,68.302,50,68.302z"></path>'
+


clarifying text

clarifying text

Former-commit-id: baec946b3755fd5322cda87b33212841d819eb06 [formerly 40a0abb2b28665f3b48091a97336520cac1147ff] [formerly e7295d39018171f5e98f6ae8b4ee8220d382a9f1 [formerly 883a9640473351ac808ee7135b402f6751fdf50a]] [formerly a1c3197cfe8e581cf3cc6fadf005928cf772b15a [formerly f52614cc58fab3c90f80678930dde5321c6e582b] [formerly ce6281cda4ec65fdab069b5fe12fb9e4ddc4ef23 [formerly ce6281cda4ec65fdab069b5fe12fb9e4ddc4ef23 [formerly ea7c689a1b6091178cf91bec1e9319aa1fbafcd3]]]]
Former-commit-id: 627b3993ee76b0b6f2369ffedf9d320ce8516ec7 [formerly 516f78f74654f15ba8db2e980f1c6672ff58fcdf] [formerly 880bc9d96c986530b140ec3905ff3b6696a76b52 [formerly d813c483db9b0d4c15df08da2b23b87133fce6b9]]
Former-commit-id: 546d8bf879fab6d57a6dac70283bf351b5f9a587 [formerly 7dee842e1d314586b20598699ccff2835bb1f8f7]
Former-commit-id: 4e65424af1d0f076d31c0b52ffabde1e280e4888
Former-commit-id: b3cf90d5f6888108c3fa37d2e41f3149d81b22cb

--- @client/roles.coffee
+++ @client/roles.coffee
@@ -236,7 +236,7 @@
           save target
           save @local

-        'Done'
+        'Done. Add these roles.'

 UsersWithRole = ReactiveComponent


verison mismatch

verison mismatch

--- lib/auth.coffee
+++ lib/auth.coffee
@@ -28,7 +28,8 @@
         response.on 'end', ->
           try
             parsed = JSON.parse(body)
-            console.log("parsed data form api.greyatom.com/v1/users/ => ", parsed)
+            console.log("parsed data form api.greyatom.com/v1/users/")
+            console.log parsed
             if parsed.data.email
               localStorage.set('commit-live:user-info', JSON.stringify(parsed.data))
               resolve parsed


cleanup

cleanup

--- models/current_user.coffee
+++ models/current_user.coffee
@@ -42,21 +42,17 @@

   # Methods for the a user's Order
   fetchPendingOrder: (options) ->
-    url = "#{@url()}/order/pending"
-    url = @formatOrderUrl url
+    url = @formatOrderUrl "#{@url()}/order/pending"
     new Order().fetch _.extend({ url: url, data: { access_token: @get('accessToken') } }, options)

   updateOrder: (orderId, options) ->
-    url = "#{@url()}/order/#{orderId}"
-    url = @formatOrderUrl url
+    url = @formatOrderUrl "#{@url()}/order/#{orderId}"
     new Order(id: orderId).save({ access_token: @get('accessToken') }, _.extend({url: url}, options))

   submitOrder: (orderId, options) ->
-    url = "#{@url()}/order/#{orderId}/submit"
-    url = @formatOrderUrl url
+    url = @formatOrderUrl "#{@url()}/order/#{orderId}/submit"
     new Order(id: orderId).save({ access_token: @get('accessToken') }, _.extend({url: url}, options))

   resumeOrder: (orderId, token, options) ->
-    url = "#{@url()}/order/#{orderId}/resume"
-    url = @formatOrderUrl url
+    url = @formatOrderUrl "#{@url()}/order/#{orderId}/resume"
     new Order(id: orderId).save({ access_token: @get('accessToken'), token: token }, _.extend({url: url}, options))


add case-insensitive modifier to sprintly respond regex

--- src/scripts/sprintly.coffee
+++ src/scripts/sprintly.coffee
@@ -33,12 +33,12 @@
         else
           msg.send "Unable to verify API token: #{body}"

-  robot.respond /sprintly +default +(\d+) *$/, (msg) ->
+  robot.respond /sprintly +default +(\d+) *$/i, (msg) ->
     robot.brain.data.sprintly ?= {}
     robot.brain.data.sprintly.product_id = msg.match[1]
     msg.send "Default Product ID set to #{msg.match[1]}"

-  robot.respond /sprintly *(?: +(\d+))?(?: +(backlog|in-progress|completed|accepted))?(?: +(\d+))? *$/, (msg) ->
+  robot.respond /sprintly *(?: +(\d+))?(?: +(backlog|in-progress|completed|accepted))?(?: +(\d+))? *$/i, (msg) ->
     query = status: msg.match[2] ? 'in-progress'
     query.limit = msg.match[3] if msg.match[3]
     sprintly(msg).product()
@@ -46,14 +46,14 @@
       .query(query)
       .get()(formatItems(msg))

-  robot.respond /sprintly +(?:(\d+) +)?mine *$/, (msg) ->
+  robot.respond /sprintly +(?:(\d+) +)?mine *$/i, (msg) ->
     withUserId msg, (user_id) ->
       sprintly(msg).product()
         .scope('items.json')
         .query(assigned_to: user_id)
         .get()(formatItems(msg))

-  robot.respond /sprintly +(?:(\d+) +)?#(\d+) *$/, (msg) ->
+  robot.respond /sprintly +(?:(\d+) +)?#(\d+) *$/i, (msg) ->
     sprintly(msg).product()
       .scope("items/#{msg.match[2]}.json")
       .get() (err, res, body) ->
@@ -71,12 +71,12 @@
         else
           msg.send "Something came up: #{body}"

-  robot.respond /sprintly +(?:(\d+) +)?#(\d+) +tasks *$/, (msg) ->
+  robot.respond /sprintly +(?:(\d+) +)?#(\d+) +tasks *$/i, (msg) ->
     sprintly(msg).product()
       .scope("items/#{msg.match[2]}/children.json")
       .get()(formatItems(msg, true))

-  robot.respond /sprintly +(?:(\d+) +)?(start|stop|finish|accept|reject|delete) +#?(\d+) *$/, (msg) ->
+  robot.respond /sprintly +(?:(\d+) +)?(start|stop|finish|accept|reject|delete) +#?(\d+) *$/i, (msg) ->
     withUserId msg, (user_id) ->
       query = {}
       method = 'post'


[Completes #35774531] there's care

[Completes #35774531] there's care

--- lib/care.coffee
+++ lib/care.coffee
@@ -1 +1,71 @@
-module.exports = {}
+# Used by lin's `Ensemble` for localization.
+# See `polyglot` from upon for details about the data format.
+
+module.exports =
+
+  "MO":
+    name:
+      en: [ "Selene",         "Moon" ]
+      el: [ true,             "Σελήνη" ]
+      bg: [ "Селена",         "Луна" ]
+  "ME":
+    name:
+      en: [ "Hermes",         "Mercury" ]
+      el: [ true,             "Ἑρμῆς" ]
+      bg: [ "Хермес",         "Меркурий" ]
+  "VE":
+    name:
+      en: [ "Aphrodite",      "Venus" ]
+      el: [ true,             "Ἀφροδίτη" ]
+      bg: [ "Афродита",       "Венера" ]
+  "SO":
+    name:
+      en: [ "Helios",         "Sun" ]
+      el: [ true,             "Ἥλιος" ]
+      bg: [ "Хелиос",         "Слънце" ]
+  "MA":
+    name:
+      en: [ "Ares",           "Mars" ]
+      el: [ true,             "Ἄρης" ]
+      bg: [ "Арес",           "Марс" ]
+  "JU":
+    name:
+      en: [ "Zeus",           "Jupiter" ]
+      el: [ true,             "Ζεύς" ]
+      bg: [ "Зевс",           "Юпитер" ]
+  "SA":
+    name:
+      en: [ "Kronos",         "Saturn" ]
+      el: [ true,             "Κρόνος" ]
+      bg: [ "Кронос",         "Сатурн" ]
+
+  "NN":
+    name:
+      en: [ true,             "North Node" ]
+  "SN":
+    name:
+      en: [ true,             "South Node" ]
+
+  "AS":
+    name:
+      en: [ false,             "Ascendant" ]
+  "MC":
+    name:
+      en: [ false,             "Midheaven" ]
+  "DS":
+    name:
+      en: [ false,             "Descendant" ]
+  "IC":
+    name:
+      en: [ false,             "Imum Coeli" ]
+
+  "UR":
+    name:
+      en: [ false,            "Uranus" ]
+  "NE":
+    name:
+      en: [ false,            "Neptune" ]
+  "PL":
+    name:
+      en: [ false,            "Pluto" ]
+


Fix events path

Fix events path

--- javascripts/dashing.coffee
+++ javascripts/dashing.coffee
@@ -92,7 +92,7 @@
 Dashing.lastEvents = lastEvents = {}
 Dashing.debugMode = false

-source = new EventSource('events')
+source = new EventSource('/events')
 source.addEventListener 'open', (e) ->
   console.log("Connection opened", e)


Stack: Temp fix for binding newly created rules to domains.

--- client/Environments/views/scene/environmentsceneview.coffee
+++ client/Environments/views/scene/environmentsceneview.coffee
@@ -90,7 +90,7 @@

     KD.remote.api.JProxyRestriction.create {
       domainName : domain.domain
-      filterId   : rule.getId()
+      filterId   : rule.getId?() or rule.filter?._id
     }, (err, restriction) ->
       if err
         return new KDNotificationView


fix for the asana task: https://app.asana.com/0/3716548652471/4193769104318

--- client/landing/app/CommonViews/linkviews/taglinkview.coffee
+++ client/landing/app/CommonViews/linkviews/taglinkview.coffee
@@ -8,13 +8,14 @@
         title     : data.title
         placement : "above"
         delayIn   : 120
-        offset    : 1
     super options, data

     data.on? "TagIsDeleted", => @destroy()

     @setClass "ttag expandable"
     @unsetClass "expandable" unless options.expandable
+
+    @on "viewAppended", => @tooltip?.setPosition()

   pistachio:->
     super "{{#(title)}}"


debug display home

debug display home

--- newebe/client/app/views/app_view.coffee
+++ newebe/client/app/views/app_view.coffee
@@ -40,8 +40,14 @@
     displayActivities: => @changeSubView @activitiesView

     displayHome: =>
-        @displayMenu()
-        @displayActivities()
+        showHome = =>
+            @displayMenu()
+            @displayActivities()
+
+        if @currentView?
+            @currentView.fadeOut showHome
+        else
+            @showHome

     displayRegisterPassword: => @changeView @registerPasswordView

@@ -84,7 +90,7 @@
                 callback()
             else
                 @displayHome()
-                @activitiesView.load()
+                @activitiesView.fetch()
         else if userState.password
             @displayLogin()
         else if userState.registered


randy

randy


Cleanup variable names in Variable class

--- src/nodes/variable.coffee
+++ src/nodes/variable.coffee
@@ -8,11 +8,11 @@
   #
   # entity - The variable's {Class}
   # node - The variable node (a {Object})
-  # smc - An object mapping the actual position of a member to its Biscotto one
+  # lineMapping - An object mapping the actual position of a member to its Biscotto one
   # options - The parser options (a {Object})
   # classType - A {Boolean} indicating if the class is a `class` or an `instance`
   # comment - The comment node (a {Object})
-  constructor: (@entity, @node, @smc, @options, @classType = false, comment = null) ->
+  constructor: (@entity, @node, @lineMapping, @options, @classType = false, comment = null) ->
     try
       @doc = new Doc(comment, @options)
       @getName()


rewrite Feed: make it an abstract class, allow for asynchronous updates

--- lib/feed.coffee
+++ lib/feed.coffee
@@ -1,8 +1,21 @@
-###
+{ EventTarget } = require("sdk/event/target")
+{ emit } = require('sdk/event/core')
+
 # Represents a feed of items to display
-interface Feed
+# @abstract
+class Feed extends EventTarget
+  # The list of currently known items
+  items : []
+
   # Updates the feed content
-  #
-  # @return [FeedItem[]] the items currently contained in the feed
-  update : ()
-###
+  # @abstract
+  requestUpdate : =>
+
+  # @event updated Raised when the feed content has been updated
+
+  # Notifies listeners of a change in feed content
+  # @private
+  onUpdated : =>
+    emit(this, "updated", this)
+
+exports.Feed = Feed


Fix Uncaught TypeError:[Cannot read property 'onDidStopChanging']

--- lib/find.coffee
+++ lib/find.coffee
@@ -30,7 +30,7 @@
     @resultsModel = new ResultsModel(@findOptions)

     @subscriptions.add atom.workspace.getCenter().observeActivePaneItem (paneItem) =>
-      if paneItem?.getBuffer?()
+      if paneItem?.getBuffer?() and paneItem?.buffer?
         @findModel.setEditor(paneItem)
       else
         @findModel.setEditor(null)


Focus project-find search input upon toggle

--- lib/find.coffee
+++ lib/find.coffee
@@ -72,6 +72,7 @@
         @projectFindPanel.hide()
       else
         @projectFindPanel.show()
+        @projectFindView.focusFindElement()

     @subscriptions.add atom.commands.add 'atom-workspace', 'project-find:show-in-current-directory', ({target}) =>
       @createViews()


Stop server and test cases on SIGINT

Stop server and test cases on SIGINT

--- lib/fran.coffee
+++ lib/fran.coffee
@@ -10,9 +10,26 @@

   modules = []
   servers = []
+  testCases = []
+
+  stop = ->
+    promises = []
+
+    servers.forEach (server) ->
+      promises.push w.promise (resolve, reject) ->
+        server.close(resolve)
+
+    testCases.forEach (testCase) ->
+      promises.push w.promise (resolve, reject) ->
+        testCase.stop().then(resolve)
+
+    w.all(promises)

   start: ->
     app.use express.static(Project.publicPath)
+
+    process.on 'SIGINT', ->
+      stop().then(process.exit)

     w.all Config.ports.map (port) ->
       w.promise (resolve, reject) ->
@@ -21,14 +38,12 @@
   use: (module) ->
     modules.push(module)

-  stop: ->
-    w.all servers.map (server) ->
-      w.promise (resolve, reject) ->
-        server.close(resolve)
+  stop: stop

   load: (name) ->
     testCase = TestCase(name)
     testCase.mount(app)
+    testCases.push(testCase)

     modules.forEach (module) ->
       testCase.use(module)


Re-work the regex

Re-work the regex

Also updates several things about the linter.

--- lib/init.coffee
+++ lib/init.coffee
@@ -13,7 +13,8 @@
     forceElixirc:
       type: 'boolean'
       title: 'Always use elixirc'
-      description: 'Activating this will force plugin to never use `mix compile` and always use `elixirc`.'
+      description: 'Activating this will force the plugin to never use ' +
+        '`mix compile` and always use `elixirc`.'
       default: false

   activate: ->
@@ -28,81 +29,105 @@
     @subscriptions.add atom.config.observe 'linter-elixirc.forceElixirc',
       (forceElixirc) =>
         @forceElixirc = forceElixirc
+
   deactivate: ->
     @subscriptions.dispose()
+
   provideLinter: ->
-    helpers = require('atom-linter')
+    helpers = require 'atom-linter'
     os = require 'os'
     fs = require 'fs'
     path = require 'path'
+
     projectPath = (textEditor) ->
       editorPath = textEditor.getPath()
       for projectPathCandidate in atom.project.getPaths()
         if editorPath.indexOf(projectPathCandidate) == 0
           return projectPathCandidate
       null
+
     isMixProject = (textEditor) ->
-      fs.existsSync(projectPath(textEditor) + '/mix.exs')
+      project = projectPath(textEditor)
+      return false if not project
+      return fs.existsSync(path.join(project, 'mix.exs'))
+
     isTestFile = (textEditor) ->
-      relativePath = path.relative(projectPath(textEditor), textEditor.getPath())
+      project = projectPath(textEditor)
+      return false if not project
+      relativePath = path.relative(project, textEditor.getPath())
       relativePath.split(path.sep)[0] == 'test'
+
     isForcedElixirc = =>
       @forceElixirc
+
     isExsFile = (textEditor) ->
       textEditor.getPath().endsWith('.exs')
+
     isPhoenixProject = (textEditor) ->
-      mixLockPath = projectPath(textEditor) + '/mix.lock'
+      project = projectPath(textEditor)
+      return false if not project
+      mixLockPath = path.join(project, 'mix.lock')
       try
         mixLockContent = fs.readFileSync mixLockPath, 'utf-8'
         mixLockContent.indexOf('"phoenix"') > 0
       catch
         false
-    parseError = (row, textEditor) ->
-      return unless row.startsWith('** ')
+
+    parseError = (toParse, textEditor) ->
+      ret = []
       re = ///
-        .*
-        \((.*)\)  # 1 - (TypeOfError)
-        \ ([^:]+) # 2 - file name
-        :(\d+):   # 3 - line
-        \ (.*)    # 4 - message
-        ///
-      reResult = re.exec(row)
-      return unless reResult?
-      ret =
-        #type: reResult[1]
-        type: "Error"
-        text: reResult[4]
-        filePath: projectPath(textEditor) + '/' + reResult[2]
-        range: helpers.rangeFromLineNumber(textEditor, reResult[3] - 1)
-    parseWarning = (row, textEditor) ->
+        \*\*.*
+        (\((.*)\)         # 1 - Full message + 2 - (TypeOfError)
+        \s(.+))           # 3 - Message
+        [\r\n]{1,2}(?:.+) # Internal elixir code
+        \s+(.+)           # 4 - File
+        :(\d+):           # 5 - Line
+        ///g
+      reResult = re.exec(toParse)
+      while reResult?
+        ret.push
+          type: "Error"
+          text: reResult[1]
+          filePath: path.join(projectPath(textEditor), reResult[4])
+          range: helpers.rangeFromLineNumber(textEditor, reResult[5] - 1)
+        reResult = re.exec(toParse)
+      ret
+
+    parseWarning = (toParse, textEditor) ->
+      ret = []
       re = ///
-        ([^:]*) # 1 - file name
-        :(\d+)  # 2 - line
+        ([^:]*) # 1 - File name
+        :(\d+)  # 2 - Line
         :\ warning
-        :\ (.*) # 3 - message
-        ///
-      reResult = re.exec(row)
-      return unless reResult?
-      ret =
-        type: "Warning"
-        text: reResult[3]
-        filePath: projectPath(textEditor) + '/' + reResult[1]
-        range: helpers.rangeFromLineNumber(textEditor, reResult[2] - 1)
+        :\ (.*) # 3 - Message
+        ///g
+      reResult = re.exec(toParse)
+      while reResult?
+        ret.push
+          type: "Warning"
+          text: reResult[3]
+          filePath: projectPath(textEditor) + '/' + reResult[1]
+          range: helpers.rangeFromLineNumber(textEditor, reResult[2] - 1)
+        reResult = re.exec(toParse)
+      ret
+
     handleResult = (textEditor) ->
       (compileResult) ->
         resultString = compileResult['stdout'] + "\n" + compileResult['stderr']
-        resultLines = resultString.split("\n")
-        errorStack = (parseError(line, textEditor) for line in resultLines unless !resultLines?)
-        warningStack = (parseWarning(line, textEditor) for line in resultLines unless !resultLines?)
+        errorStack = parseError(resultString, textEditor)
+        warningStack = parseWarning(resultString, textEditor)
         (error for error in errorStack.concat(warningStack) when error?)
+
     getFilePathDir = (textEditor) ->
       filePath = textEditor.getPath()
       path.dirname(filePath)
+
     getOpts = (textEditor) ->
       opts =
         cwd: projectPath(textEditor)
         throwOnStdErr: false
         stream: 'both'
+
     getDepsPa = (textEditor) ->
       env = if isTestFile(textEditor) then "test" else "dev"
       buildDir = path.join("_build", env, "lib")
@@ -111,25 +136,31 @@
           path.join(projectPath(textEditor), buildDir, item, "ebin")
       catch e
         []
+
     lintElixirc = (textEditor) =>
       elixircArgs = [
-        "--ignore-module-conflict", "--app", "mix", "--app", "ex_unit", "-o", os.tmpDir(),
+        "--ignore-module-conflict",
+        "--app", "mix",
+        "--app", "ex_unit",
+        "-o", os.tmpDir(),
       ]
       elixircArgs.push "-pa", item for item in getDepsPa(textEditor)
       elixircArgs.push textEditor.getPath()
       helpers.exec(@elixircPath, elixircArgs, getOpts(textEditor))
         .then(handleResult(textEditor))
+
     lintMix = (textEditor) =>
       helpers.exec(@mixPath, ['compile'], getOpts(textEditor))
         .then (handleResult(textEditor))

     provider =
       grammarScopes: ['source.elixir']
-      scope: 'file'
+      scope: 'project'
       lintOnFly: false
       name: 'Elixir'
-      lint: (textEditor) =>
-        if isForcedElixirc() or not isMixProject(textEditor) or isExsFile(textEditor) or isPhoenixProject(textEditor)
-          lintElixirc(textEditor)
+      lint: (textEditor) ->
+        if isForcedElixirc() or not isMixProject(textEditor) or
+          isExsFile(textEditor) or isPhoenixProject(textEditor)
+            lintElixirc(textEditor)
         else
           lintMix(textEditor)


Add `description` to configurations

Add `description` to configurations

--- lib/init.coffee
+++ lib/init.coffee
@@ -2,16 +2,17 @@
   config:
     configurationPath:
       type: 'string'
-      title: 'the path to SwiftLint\'s configuration file that relative to project or absolute'
+      title: 'Configuration Path'
+      description: 'SwiftLint\'s configuration file that relative to project or absolute'
       default: '.swiftlint.yml'
     additionalOptions:
       type: 'string'
-      title: 'additional options to SwiftLint\'s `lint` command
-              other than `--config` and `--use-stdin`'
+      title: 'Additional Options'
+      description: 'SwiftLint\'s `lint` command options other than `--config` and `--use-stdin`'
       default: ''
     swiftlintExecutablePath:
       type: 'string'
-      title: 'the path to SwiftLint'
+      title: 'The Path to SwiftLint'
       default: '/usr/local/bin/swiftlint'

   activate: ->


:bug: Fix removed commands in activate

:bug: Fix removed commands in activate

--- lib/main.coffee
+++ lib/main.coffee
@@ -95,6 +95,10 @@

     MinimapElement.registerViewProvider()

+    @subscriptions.add atom.commands.add 'atom-workspace',
+      'minimap:toggle': => @toggle()
+      'minimap:generate-plugin': => @generatePlugin()
+
     @toggle() if atom.config.get 'minimap.autoToggle'

   # Deactivates the minimap package.


:art: remove unnecessary registry. Formerly both Set and WeakMap was used.

--- lib/main.coffee
+++ lib/main.coffee
@@ -28,21 +28,16 @@
     @subscriptions = new CompositeDisposable
     @statusBarManager = new StatusBarManager
     @registerViewProviders()
-
-    @vimStates = new Set
-    @vimStatesByEditor = new WeakMap
-
+    @editorStates = new Map
     @subscriptions.add atom.workspace.observeTextEditors (editor) =>
-      return if editor.isMini() or @vimStatesByEditor.get(editor)
-
+      return if editor.isMini() or @editorStates.has(editor)
       vimState = new VimState(editor, @statusBarManager)
-      @vimStates.add(vimState)
-      @vimStatesByEditor.set(editor, vimState)
+      @editorStates.set(editor, vimState)
       vimState.onDidDestroy =>
-        @vimStates.delete(vimState)
+        @editorStates.delete(editor)

     @subscriptions.add new Disposable =>
-      @vimStates.forEach (vimState) -> vimState.destroy()
+      @editorStates.forEach (vimState) -> vimState.destroy()

     @subscriptions.add Base.init(@provideVimModePlus())
     @registerCommands()
@@ -85,7 +80,7 @@
     globalState

   getEditorState: (editor) ->
-    @vimStatesByEditor.get(editor)
+    @editorStates.get(editor)

   consumeStatusBar: (statusBar) ->
     @statusBarManager.initialize(statusBar)


toggle-logs -> toggle-log

toggle-logs -> toggle-log

--- lib/main.coffee
+++ lib/main.coffee
@@ -60,7 +60,7 @@

     @notificationsLog = new NotificationsLog
     @subscriptions.add @notificationsLog.onItemClick @logItemClick.bind(this)
-    @subscriptions.add atom.commands.add 'atom-workspace', 'notifications:toggle-logs', => @notificationsLog.toggle()
+    @subscriptions.add atom.commands.add 'atom-workspace', 'notifications:toggle-log', => @notificationsLog.toggle()

   deactivate: ->
     @subscriptions.dispose()


Wrong parameters for _getModel

Wrong parameters for _getModel

--- lib/misc.coffee
+++ lib/misc.coffee
@@ -22,7 +22,7 @@

   # since we already assume that any instance of BaseClass uses a MapManager
   # We can create it here, to save lines of code
-  _getModel: (@_model)->
+  _getModel: (Y, Operation)->
     if not @_model?
       @_model = new Operation.MapManager(@).execute()
       for n,v of @_tmp_model


:art: Refactor the code a bit

:art: Refactor the code a bit

--- lib/plus.coffee
+++ lib/plus.coffee
@@ -38,13 +38,9 @@
       return unless @LintOnFly
       @SubLintOnFly.add editor.onDidStopChanging @lint.bind(@, true)
   lint:(onChange)->
-    if onChange
-      return if @InProgressFly
-      @InProgressFly = true
-    else
-      return if @InProgress
-      @InProgress = true
     onChange = Boolean onChange
+    return if @progress onChange
+    # We need to consume both onFly and Regular linters on save
     @lint(true) unless onChange

     ActiveEditor = atom.workspace.getActiveTextEditor()
@@ -62,10 +58,7 @@
       else if RetVal
         Promises.push RetVal
     Promise.all(Promises).then (Results)=>
-      if onChange
-        @InProgressFly = false
-      else
-        @InProgress = false
+      @progress onChange, false
       Messages = []
       for Result in Results
         continue if (not Result) or (typeof Result) isnt 'object'
@@ -81,10 +74,7 @@
       @render()
     , =>
       console.error arguments
-      if onChange
-        @InProgressFly = false
-      else
-        @InProgress = false
+      @progress onChange, false
   render:->
     if not @Messages.length
       @ViewPanel.hide() if @ViewPanel.isVisible()
@@ -97,5 +87,16 @@
     @View.remove()
     @SubLintOnFly.dispose()
     @Subscriptions.dispose()
+  progress:(onChange, newValue)->
+    if typeof newValue is 'undefined'
+      if onChange
+        return @InProgressFly
+      else
+        return @InProgress
+    else
+      if onChange
+        @InProgressFly = newValue
+      else
+        @InProgress = newValue

 module.exports = LinterPlus


Fix unknown error handling.

Fix unknown error handling.

--- lib/sifu.coffee
+++ lib/sifu.coffee
@@ -82,12 +82,12 @@
           filePath: textEditor.getPath()   # Full path to file
           range: [[lineNum, 0], [lineNum, 0]] # Set range-beginnings

-      match3 = line.match /.*\[ERROR\](.*)/
+      match3 = line.match /.*\[ERROR\][^ ]*Unknown(.*)/
       if !!match3
         mess = match3[1]
         errors.push
           type: "error"
-          text: mess.replace /\[\d+m/g, ''
+          text: 'Unknown internal error occurred.', ''
           filePath: textEditor.getPath()   # Full path to file
           range: [[0, 0], [0, 0]] # Set range-beginnings


change comment symbol; support both shorthand and fullname for node type

--- tools/script_parser/parser.coffee
+++ tools/script_parser/parser.coffee
@@ -20,7 +20,7 @@

 reader.on 'line', (line) ->
   # comment
-  if line[0] is '#'
+  if line.startsWith '//'
     return

   # conclude options block for the current node
@@ -42,9 +42,9 @@
   if current_type is undefined
     # waiting for a symbol
     switch line
-      when 'v'
+      when 'VIDEO', 'V'
         current_type = 'video'
-      when 'n'
+      when 'NARRATION', 'N'
         current_type = 'narrate'
       # TODO
       # when 'options'


set choice's role to 'button' for accessibility

--- tutor/src/components/new-course/choice.cjsx
+++ tutor/src/components/new-course/choice.cjsx
@@ -14,6 +14,7 @@

   render: ->
     <div
+      role="button"
       className={classnames("list-group-item", "choice", @props.className, {
         active: @props.active, disabled: @props.disabled
       })}


use default export

use default export

--- tutor/src/helpers/conditional-handlers.cjsx
+++ tutor/src/helpers/conditional-handlers.cjsx
@@ -3,7 +3,7 @@
 {Redirect, Link} = require 'react-router'

 TutorRouter = require '../helpers/router'
-InvalidPage = require '../components/invalid-page'
+{default: InvalidPage} = require '../components/invalid-page'

 TeacherTaskPlans        = require '../components/task-plan/teacher-task-plans-listing'
 {default: CCStudentRedirect}       = require '../components/cc-student-redirect'


remove padding

remove padding

--- keymaps/vim-mode-plus.cson
+++ keymaps/vim-mode-plus.cson
@@ -332,15 +332,15 @@
   'i f':  'vim-mode-plus:inner-function'
   'a f':  'vim-mode-plus:a-function'

-  'i l':  'vim-mode-plus:inner-current-line'
-  'a l':  'vim-mode-plus:a-current-line'
-
-  'i e':  'vim-mode-plus:inner-entire'
-  'a e':  'vim-mode-plus:a-entire'
+  'i l': 'vim-mode-plus:inner-current-line'
+  'a l': 'vim-mode-plus:a-current-line'
+
+  'i e': 'vim-mode-plus:inner-entire'
+  'a e': 'vim-mode-plus:a-entire'

   # [NOTE] subject to change in futur
-  'i c':  'vim-mode-plus:inner-latest-change'
-  'a c':  'vim-mode-plus:a-latest-change'
+  'i c': 'vim-mode-plus:inner-latest-change'
+  'a c': 'vim-mode-plus:a-latest-change'

 # visual
 # -------------------------
@@ -353,6 +353,7 @@
   'v': 'vim-mode-plus:activate-characterwise-visual-mode'
   'V': 'vim-mode-plus:activate-linewise-visual-mode'
   'ctrl-v': 'vim-mode-plus:activate-blockwise-visual-mode'
+
   # 'ctrl-k': 'vim-mode-plus:move-line-up'
   # 'ctrl-j': 'vim-mode-plus:move-line-down'


Allow numpad0 to be used when entering a number before a command

Discovered that this wasn't working and then used the keybinding debugger (ctrl+.) to figure out the issue. Checked whether this works by adding this line to my local keymap. It fixes the problem.

Without this, typing something like `420G` fails while typing `0` because `numpad0` isn't mapped correctly. The `numpad0` key always takes you to the front of the line without this change.

--- keymaps/vim-mode-plus.cson
+++ keymaps/vim-mode-plus.cson
@@ -429,6 +429,7 @@

 'atom-text-editor.vim-mode-plus.with-count:not(.insert-mode)':
   '0': 'vim-mode-plus:set-count-0'
+  'numpad0': 'vim-mode-plus:set-count-0'
   '%': 'vim-mode-plus:move-to-line-by-percent'

 'atom-text-editor.vim-mode-plus.has-persistent-selection:not(.insert-mode)':


resurrection.teamwork: fix activity input widget submit handler

--- client/Main/CommonViews/activitywidget.coffee
+++ client/Main/CommonViews/activitywidget.coffee
@@ -7,10 +7,7 @@

   showForm: (callback) ->
     @inputWidget?.show()
-    @inputWidget.once "Submit", (err, activity) =>
-      return  KD.showError err if err
-      @addActivity activity  if activity
-      callback err, activity
+    @inputWidget.once "Submit", callback

   hideForm: ->
     @inputWidget?.destroy()
@@ -38,3 +35,6 @@
     {defaultValue} = @getOptions()
     KD.singleton("appManager").create "Activity", =>
       @addSubView @inputWidget = new ActivityInputWidget {defaultValue}
+      @inputWidget.once "Submit", (err, activity) =>
+        return  KD.showError err if err
+        @addActivity activity  if activity


More specs

More specs

--- src/spec/sphere.spec.coffee
+++ src/spec/sphere.spec.coffee
@@ -10,4 +10,25 @@
   it 'should output help when no options are passed', (done)->
     runCommand '', (error, result)->
       expect(result).toMatch /Usage\: sphere \[options\] \[command\]/
-      done()+      done()
+
+  describe ':: auth', ->
+
+    it 'should output help when no options are passed', (done)->
+      runCommand 'auth', (error, result)->
+        expect(result).toMatch /Usage\: sphere-auth \[options\] \[command\]/
+        done()
+
+  describe ':: products', ->
+
+    it 'should output help when no options are passed', (done)->
+      runCommand 'products', (error, result)->
+        expect(result).toMatch /Usage\: sphere-products \[options\] \[command\]/
+        done()
+
+  describe ':: product-types', ->
+
+    it 'should output help when no options are passed', (done)->
+      runCommand 'product-types', (error, result)->
+        expect(result).toMatch /Usage\: sphere-product-types \[options\] \[command\]/
+        done()


accountpopup: write plan name with lowercase `p`

--- client/app/lib/avatararea/accountpopup.coffee
+++ client/app/lib/avatararea/accountpopup.coffee
@@ -15,7 +15,7 @@
     { groupsController } = kd.singletons

     @avatarPopupContent.addSubView @paymentActionLabel = new CustomLinkView
-      title      : 'Upgrade Plan'
+      title      : 'Upgrade plan'
       href       : '/Pricing'
       cssClass   : 'bottom-separator'
       click      : @bound 'goToPricing'
@@ -82,11 +82,11 @@

     paymentController.subscriptions (err, subscription) =>
       title = if err or not subscription
-      then 'Upgrade Plan'
+      then 'Upgrade plan'
       else
         if subscription.planTitle is 'free'
-        then 'Upgrade Plan'
-        else 'Change Plan'
+        then 'Upgrade plan'
+        else 'Change plan'

       @paymentActionLabel.updatePartial title


client: avoid uploading if more than length

--- client/app/lib/commonviews/dnduploader.coffee
+++ client/app/lib/commonviews/dnduploader.coffee
@@ -80,6 +80,7 @@

     if files.length >= 20
       @showGenericError()
+      return

     if items?[0].webkitGetAsEntry?


Collaboration: Copy change.

Collaboration: Copy change.

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -857,7 +857,7 @@

     modal.container.addSubView new kd.CustomHTMLView
       tagName  : 'p'
-      partial  : " Joining to collaboration session..."
+      partial  : " Joining collaboration session..."
       cssClass : "state-label running"

     modal.container.addSubView modal.progressBar = new kd.ProgressBarView { initial: 10 }


Fix faulty tests

Fix faulty tests

--- client/src/tests/frame-list-view.tests.coffee
+++ client/src/tests/frame-list-view.tests.coffee
@@ -38,12 +38,10 @@
       t.equal frameListView.items().length, 2, 'frameListView.items() array lengths match'
       t.equal frameListView.items()[0].title, 'airlines_test.hex', 'String frameListView.items()[0].title equals [airlines_test.hex]'
       t.equal frameListView.items()[0].caption, '13 columns: fYear, fMonth, fDayofMonth, fDayOfWeek, DepTime', 'String frameListView.items()[0].caption equals [13 columns: fYear, fMonth, fDayofMonth, fDayOfWeek, DepTime]'
-      t.equal frameListView.items()[0].timestamp, 1400170833012, 'Number frameListView.items()[0].timestamp equals [1400170833012]'
       t.ok (isFunction frameListView.items()[0].display), 'frameListView.items()[0].display is a function'
       t.equal frameListView.items()[0].isActive(), true, 'Boolean frameListView.items()[0].isActive() equals [true]'
       t.equal frameListView.items()[1].title, 'airlines_train.hex', 'String frameListView.items()[1].title equals [airlines_train.hex]'
       t.equal frameListView.items()[1].caption, '13 columns: fYear, fMonth, fDayofMonth, fDayOfWeek, DepTime', 'String frameListView.items()[1].caption equals [13 columns: fYear, fMonth, fDayofMonth, fDayOfWeek, DepTime]'
-      t.equal frameListView.items()[1].timestamp, 1400170833013, 'Number frameListView.items()[1].timestamp equals [1400170833013]'
       t.ok (isFunction frameListView.items()[1].display), 'frameListView.items()[1].display is a function'
       t.equal frameListView.items()[1].isActive(), false, 'Boolean frameListView.items()[1].isActive() equals [false]'
       t.equal frameListView.predicateCaption(), 'Showing datasets compatible with\ndl_AirlinesTrain_binary_1', 'String frameListView.predicateCaption() equals [Showing datasets compatible with\ndl_AirlinesTrain_binary_1]'


validate program

validate program

--- src/test/test_config.coffee
+++ src/test/test_config.coffee
@@ -29,7 +29,7 @@
     context = world.find('.controls.program_loader.conflict')
     t.ok context, "context"
     console.log context
-    program = context.get_raw('program')
+    program = context.get('program')
     console.log program
-    t.ok _.isArray program
+    t.ok world.is_array program
     t.end()


--- src/node_modules/round-contributors/round-contributors-controller.coffee
+++ src/node_modules/round-contributors/round-contributors-controller.coffee
@@ -4,8 +4,11 @@
   contributors = round.getContributors(memberships)
   allocations = round.getAllocationsByUser()

-  $scope.contributors = contributors
   $scope.isAdmin = group.isAdmin(AuthService.getCurrentUser())
+
+  $scope.contributors = _.filter contributors, (contributor) ->
+    # non-admins shouldn't see contributors with no allocations
+    $scope.isAdmin or contributor.allocationCents != 0

   $scope.updateAllocation = (contributor, data) ->
     allocation = allocations[contributor.user.id]


starting to find the real IP to tell the user

--- routes/index.coffee
+++ routes/index.coffee
@@ -1,6 +1,8 @@
 express = require("express")
 router = express.Router()
 applescript = require("applescript")
+os = require("os")
+networkInterfaces = os.networkInterfaces()

 # Navigation functions
 goToSlide = (direction) ->
@@ -14,11 +16,23 @@
     console.log err  if err
     return

+publicIP = () ->
+  ips = []
+  for interfaceName, inetList of networkInterfaces
+    for inet in inetList
+      # Not using a hash b/c we care about order as our priority.
+      if !inet["internal"] && inet["family"].match(/IPv4/i)
+        ips.push [interfaceName, inet["address"]]
+
+  return ips
+
 # GET home page.
 router.get "/", (req, res) ->
-  res.render "index",
-    title: "PowerPoint Server"
-
+  res.json({
+    success: true,
+    text: "Enter #{req.protocol}://#{req.get('host')}/} into your prefs",
+    ifconfig: publicIP()
+  })
   return

 # GET to a slide direction
@@ -30,6 +44,6 @@
     return
   else
     console.error("Invalid parameter: #{direction}")
-    res.send(400, 'Invalid parameters');
+    res.send(400, 'Invalid parameters')

 module.exports = router


Defer updates of remote ops until Ace has been updated

--- services/web/public/coffee/ide/editor/Document.coffee
+++ services/web/public/coffee/ide/editor/Document.coffee
@@ -353,7 +353,12 @@
 				@ranges.applyOp op, { user_id: track_changes_as }
 			if old_id_seed?
 				@ranges.setIdSeed(old_id_seed)
-			@emit "ranges:dirty"
+			if remote_op
+				# With remote ops, Ace hasn't been updated when we receive this op,
+				# so defer updating track changes until it has
+				setTimeout () => @emit "ranges:dirty"
+			else
+				@emit "ranges:dirty"

 		_catchUpRanges: (changes = [], comments = []) ->
 			# We've just been given the current server's ranges, but need to apply any local ops we have.


Added BANKTRANSFER method constant.

Added BANKTRANSFER method constant.

--- src/lib/mollie/api/object/method.coffee
+++ src/lib/mollie/api/object/method.coffee
@@ -31,11 +31,12 @@
 Mollie = API: Object: {}

 module.exports = class Mollie.API.Object.Method
-  @IDEAL       = "ideal";
-  @PAYSAFECARD = "paysafecard";
-  @CREDITCARD  = "creditcard";
-  @MISTERCASH  = "mistercash";
-  @PAYPAL      = "paypal";
+  @IDEAL        = "ideal"
+  @PAYSAFECARD  = "paysafecard"
+  @CREDITCARD   = "creditcard"
+  @MISTERCASH   = "mistercash"
+  @BANKTRANSFER = "banktransfer"
+  @PAYPAL       = "paypal"

   constructor: ->
     @id = null


Replace CS 'in' operator with includes for performance

--- src/underscore-query.coffee
+++ src/underscore-query.coffee
@@ -41,6 +41,8 @@
     (item for item in array1 when array2.indexOf(item) isnt -1)

   out.isEqual = (a,b) -> JSON.stringify(a) is JSON.stringify(b)
+
+  out.includes = (a, b) -> b in a
   out

@@ -48,7 +50,7 @@
 # If underscore is not supplied we use the above ES5 methods
 createUtils = (_) ->
   for key in ["every", "some", "filter", "first", "find", "reject", "reduce",
-      "intersection", "isEqual", "keys", "isArray", "result", "map"]
+      "intersection", "isEqual", "keys", "isArray", "result", "map", "includes"]
     utils[key] = _[key]
     throw new Error("#{key} missing. Please ensure that you first initialize
       underscore-query with either lodash or underscore") unless utils[key]
@@ -107,7 +109,7 @@

       when "Object"
       # If the key is one of the compound keys, then parse the param as a raw query
-        if key in utils.compoundKeys
+        if utils.includes(utils.compoundKeys, key)
           o.type = key
           o.value = parseSubQuery queryParam, key
           o.key = null
@@ -142,7 +144,7 @@
         o.value = queryParam

     # For "$equal" queries with arrays or objects we need to perform a deep equal
-    if (o.type is "$equal") and (paramType in ["Object","Array"])
+    if (o.type is "$equal") and (utils.includes(["Object", "Array"], paramType))
       o.type = "$deepEqual"

     result.push(o)
@@ -188,7 +190,7 @@
   switch queryType
     when "$like", "$likeI", "$regex", "$startsWith", "$endsWith"  then valueType is "String"
     when "$contains", "$all", "$any", "$elemMatch" then valueType is "Array"
-    when "$size"                      then valueType in ["String","Array"]
+    when "$size"                      then utils.includes(["String","Array"], valueType)
     when "$in", "$nin"                then value?
     else true

@@ -197,9 +199,9 @@
   switch type
     when "$equal"
       # If the attribute is an array then search for the query value in the array the same as Mongo
-      if utils.isArray(attr) then (value in attr) else (attr is value)
+      if utils.isArray(attr) then utils.includes(attr, value) else (attr is value)
     when "$deepEqual"       then utils.isEqual(attr, value)
-    when "$contains"        then value in attr
+    when "$contains"        then utils.includes(attr, value)
     when "$ne"              then attr isnt value
     when "$lt"              then value? and attr < value
     when "$gt"              then value? and attr > value
@@ -207,10 +209,10 @@
     when "$gte"             then value? and attr >= value
     when "$between"         then value[0]? and value[1]? and value[0] < attr < value[1]
     when "$betweene"        then value[0]? and value[1]? and value[0] <= attr <= value[1]
-    when "$in"              then attr in value
-    when "$nin"             then attr not in value
-    when "$all"             then utils.every value, (item) -> item in attr
-    when "$any"             then utils.some attr, (item) -> item in value
+    when "$in"              then utils.includes(value, attr)
+    when "$nin"             then not utils.includes(value, attr)
+    when "$all"             then utils.every value, (item) -> utils.includes(attr, item)
+    when "$any"             then utils.some attr, (item) -> utils.includes(value, item)
     when "$size"            then attr.length is value
     when "$exists", "$has"  then attr? is value
     when "$like"            then attr.indexOf(value) isnt -1
@@ -309,7 +311,7 @@
   compoundQuery = utils.intersection utils.compoundKeys, queryKeys

   for type in compoundQuery
-    if type in utils.expectedArrayQueries and !utils.isArray(query[type])
+    if not utils.isArray(query[type]) and utils.includes(utils.expectedArrayQueries, type)
       throw new Error(type + ' query must be an array')

   # If no compound methods are found then use the "and" iterator
@@ -319,10 +321,10 @@
     # find if there is an implicit $and compundQuery operator
     if compoundQuery.length isnt queryKeys.length
       # Add the and compund query operator (with a sanity check that it doesn't exist)
-      if "$and" not in compoundQuery
+      if not utils.includes(compoundQuery, "$and")
         query.$and = {}
         compoundQuery.unshift "$and"
-      for own key, val of query when key not in utils.compoundKeys
+      for own key, val of query when not utils.includes(utils.compundKeys, key)
         query.$and[key] = val
         delete query[key]
     (for type in compoundQuery


Fix stupid error introduced when fixing linter 😩

--- framer/Utils.coffee
+++ framer/Utils.coffee
@@ -14,7 +14,8 @@
 Utils.getValueForKeyPath = (obj, key) ->
 	result = obj
 	return obj[key] if not "." in key
-	result = (result[key] for key in key.split("."))
+	for key in key.split(".")
+		result = result[key]
 	result

 Utils.setValueForKeyPath = (obj, path, val) ->


Fixed bind for modal on hide.

Fixed bind for modal on hide.

--- app/assets/javascripts/app/controllers/_application_controller.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller.js.coffee
@@ -473,7 +473,7 @@
     data = $.extend({}, defaults, params)
     @el.modal(data)

-    @el.bind('hidden', =>
+    @el.bind('hidden.bs.modal', =>

       # navigate back to home page
 #      if @pageData && @pageData.home


Handling JSON error responses in InfrastructureBooster

--- app/assets/javascripts/infrastructures/infrastructures_booster.js.coffee
+++ app/assets/javascripts/infrastructures/infrastructures_booster.js.coffee
@@ -36,8 +36,6 @@
         window.Notices.show_loading_notice()
       )
       .bind('ajax:success', (status, data, xhr) =>
-        window.Notices.hide_notice()
-
         switch data.status
           when 'error'
             toastr.error(data.msg)
@@ -46,7 +44,12 @@
           else
             toastr.error(data.msg)
       )
+      .bind('ajax:error', (xhr, data, error) =>
+        resp = data.responseJSON
+        toastr.error(resp and (resp.reason or resp.msg) or "Unknown error - please contact administrators.")
+      )
       .bind('ajax:complete', =>
+        window.Notices.hide_notice()
         window.infrastructuresTree and window.infrastructuresTree.updateInfrastructureNode(@infrastructureName)
         window.retrieveComputationalResourcesSummary and window.retrieveComputationalResourcesSummary()
       )


empty input upon success as well

empty input upon success as well

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -40,6 +40,7 @@
         if attempt.toLowerCase() == @current.toLowerCase()
           alert("SUCCESS!")
           update_question(@data)
+          emptyInput()
         else
           @fail += 1
           $(".alert").addClass("alert-danger")


FIX: TextInput flicker further reduced

FIX: TextInput flicker further reduced

--- source/Art/Engine/Forms/SynchronizedDomOverlay.coffee
+++ source/Art/Engine/Forms/SynchronizedDomOverlay.coffee
@@ -106,4 +106,5 @@
     if canvasElement
       @_attachedToCanvasElement = canvasElement
       @_shouldAttachDomElement = true
+      @_updateDomLayout()
       @_queueUpdate()


Handle progress_bar in AsyncNotifications helper

--- lib/assets/javascripts/async_notifications/helper.js.coffee
+++ lib/assets/javascripts/async_notifications/helper.js.coffee
@@ -13,6 +13,8 @@
       displayGrowlMessage(data)
     else if data.event_type == 'view_refresh'
       triggerViewRefresh(data)
+    else if data.event_type == 'progress_bar'
+      updateProgressBar(data)

 displayGrowlMessage = (data) ->
@@ -55,3 +57,11 @@

 refreshView = (url) ->
   $.ajax({ url: url, dataType: 'script' })
+
+
+updateProgressBar = (data) ->
+  progressBar = $('#' + data.target)
+  if progressBar.length > 0
+    progressBar.parent().removeClass('hide')
+    progressBar.width(data.progress + '%')
+    progressBar.html(data.progress + '%')


Check if dynamic filters are set

Check if dynamic filters are set

--- src/core/javascripts/controllers/event_list.js.coffee
+++ src/core/javascripts/controllers/event_list.js.coffee
@@ -252,7 +252,7 @@

   filterEventsWithDynamicFilters = (item) ->

-    return true if !$scope.has_company_questions or ($scope.dynamic_filters and !$scope.dynamic_filters.question_types)
+    return true if !$scope.has_company_questions or !$scope.dynamic_filters

     result = true


Add base for light box

Add base for light box

--- client/main.coffee
+++ client/main.coffee
@@ -28,6 +28,18 @@
   hello: ->
     alert "hello"

+class LightBox extends Backbone.View
+
+  error: (msg) ->
+    alert "ERROR: #{ msg }"
+
+  warning: (msg) ->
+    alert "WARNING: #{ msg }"
+
+  info: (msg) ->
+    alert "INFO: #{ msg }"
+
+  hide: ->

 class Editor extends Backbone.View
   el: ".edit_view"
@@ -53,8 +65,8 @@
         if not @hasEditUrl()
           window.location.hash = "#edit/#{ @model.get("id") }"

-      error: (e) =>
-        console.log "failed to save", @model, e
+      error: (e, err) =>
+        console.log "failed to save", @model, e, err

   hasEditUrl: -> !!window.location.hash

@@ -62,6 +74,7 @@

   constructor: (opts) ->
    super opts
+   messages = new LightBox

   routes:
     "": "start"


improve gauges script a bit

improve gauges script a bit

--- src/scripts/gauges.coffee
+++ src/scripts/gauges.coffee
@@ -19,8 +19,6 @@
 #         robot.respond /emit gauges/i, (msg) ->
 #            robot.emit "gauges", { user: msg.user, for: 'today' }
 #
-#
-#
 # Author:
 #   tombell

@@ -29,88 +27,70 @@

   getViewsForToday: (callback) ->
     @getGauges (err, data) ->
-      if err? or not data?
-        callback err
-      else
-        gauges = []
-        for g in data.gauges
-          gauges.push
-            title: g.title
-            views: g.today.views
-            people: g.today.people
-        callback null, gauges
+      return callback err if err
+      callback null, data.gauges.map (g) ->
+        title: g.title
+        views: g.today.views
+        people: g.today.people

   getViewsForYesterday: (callback) ->
     @getGauges (err, data) ->
-      if err? or not data?
-        callback err
-      else
-        gauges = []
-        for g in data.gauges
-          gauges.push
-            title: g.title
-            views: g.yesterday.views
-            people: g.yesterday.people
-
-        callback null, gauges
+      return callback err if err
+      callback null, data.gauges.map (g) ->
+        title: g.title
+        views: g.yesterday.views
+        people: g.yesterday.people

   getViewsForDate: (date, callback) ->
     @getGauges (err, data) ->
-      if err? or not data?
-        callback err
-      else
-        gauges = []
-        for g in data.gauges
-          for d in g.recent_days
-            if d.date is date
-              gauges.push
-                title: g.title
-                views: d.views
-                people: d.people
-
-        callback null, gauges
+      return callback err if err
+      gauges = []
+      for g in data.gauges
+        days = g.recent_days.filter (d) ->
+          d.date is date
+        gauges.push days.map (d) ->
+          title: g.title
+          views: d.views
+          people: d.people
+      callback null, gauges

   getGauges: (callback) ->
     @robot.http("https://secure.gaug.es/gauges")
       .headers("X-Gauges-Token": @token)
       .get() (err, res, body) ->
+        return callback err if err
+
         if res.statusCode is 200
-          data = JSON.parse body
-          callback null, data
-        else if err?
-          callback err
+          try
+            data = JSON.parse body
+            callback null, data
+          catch err
+            callback err
         else
           callback "Could not get gauges for today"
-

 module.exports = (robot) ->

   robot.respond /gauges for (today|yesterday)/i, (msg) ->
     day = msg.match[1]
-    robot.emit "gauges", { user: msg.user, for: day }
+    robot.emit "gauges", { msg: msg, for: day }

   robot.respond /gauges for (\d{4}-\d{2}-\d{2})/i, (msg) ->
     day = msg.match[1]
-    robot.emit "gauges", { user: msg.user, for: 'data' , day: day }
+    robot.emit "gauges", { msg: msg, for: 'data' , day: day }

-  # handle gauges events, can be emited by other script
   robot.on "gauges", (data) ->
     gauges = new Gauges robot, process.env.HUBOT_GAUGES_TOKEN

-    #handle a list of gauges
     handler = (err, list) ->
-      if err?
-        robot.send data.user, "#{err}"
-      else
-        for g in list
-          robot.send data.user, "#{g.title}: Views #{g.views} People #{g.people}"
+      return data.msg.send "An error occured: #{err}" if err
+      for g in list
+        data.msg.send "#{g.title}: Views #{g.views} People #{g.people}"

     switch data.for
       when "today"
         gauges.getViewsForToday handler
-
       when "yesterday"
         gauges.getViewsForYesterday handler
-
       when "data"
-          gauges.getViewsForDate data.day, handler
+        gauges.getViewsForDate data.day, handler


Gitlab v4.0 compatibility

Gitlab v4.0 compatibility

--- src/scripts/gitlab.coffee
+++ src/scripts/gitlab.coffee
@@ -77,14 +77,14 @@
         branch = hook.ref.split("/")[2..].join("/")
         # if the ref before the commit is 00000, this is a new branch
         if /^0+$/.test(hook.before)
-            message.push "Gitlab #{bold(hook.repository.name)} (#{underline(hook.repository.url)}): #{bold(hook.user_name)} pushed a new branch: #{bold(branch)}"
+            message.push "Gitlab #{bold(hook.repository.name)} (#{underline(hook.repository.homepage)}): #{bold(hook.user_name)} pushed a new branch: #{bold(branch)}"
         else
-            message.push "Gitlab #{bold(hook.repository.name)} (#{underline(hook.repository.url)}): #{bold(hook.user_name)} pushed #{bold(hook.total_commits_count)} commits to #{bold(branch)}:"
+            message.push "Gitlab #{bold(hook.repository.name)} (#{underline(hook.repository.homepage)}): #{bold(hook.user_name)} pushed #{bold(hook.total_commits_count)} commits to #{bold(branch)}:"
             for i, commit of hook.commits
               commit_message = commit.message.split("\n")[0]
               message.push "    * #{commit_message} (#{underline(trim_commit_url(commit.url))})"
             if hook.total_commits_count > 1
-                message.push "Entire diff: #{underline(hook.repository.url + '/compare/' + hook.before.substr(0,9) + '...' + hook.after.substr(0,9))}"
+                message.push "Entire diff: #{underline(hook.repository.homepage + '/compare/' + hook.before.substr(0,9) + '...' + hook.after.substr(0,9))}"
         message = message.join("\n")
         robot.send user, message


can omit braces

can omit braces

--- src/selectors/libs.coffee
+++ src/selectors/libs.coffee
@@ -3,7 +3,7 @@
 {$query, $libs} = require 'selectors'

 module.exports =
-  filteredByQuery: cs [$query, $libs], (query, libs) ->
+  filteredByQuery: cs $query, $libs, (query, libs) ->
     query = query.trim().toLowerCase()
     if query.length
       libs = libs.filter (lib) -> lib.name.toLowerCase().match query


update js lib to send 0 when there is no sequence number, not -1

--- lib/assets/javascripts/firehose/long_poll.js.coffee
+++ lib/assets/javascripts/firehose/long_poll.js.coffee
@@ -71,9 +71,8 @@
           @_lastMessageSequence = jqXhr.getResponseHeader(@messageSequenceHeader)
       beforeSend: (jqXhr) =>
         # Tell the server what our last sequence was if we got one from the last response.
-        # TODO change this to nil or something else if no sequence is specified? Left it at -1 for now to make
-        # testing thing thing a little easier.
-        jqXhr.setRequestHeader(@messageSequenceHeader, @_lastMessageSequence ||= '-1')
+        # Zero means we have no sequence number.
+        jqXhr.setRequestHeader(@messageSequenceHeader, @_lastMessageSequence ||= '0')

   _success: (data, status, jqXhr) =>
     # TODO we actually want to do this when the thing calls out... mmm right now it takes
@@ -98,4 +97,4 @@
     clearTimeout(@_offlineTimer)
     @config.disconnected()
     @_offlineTimer = setTimeout(@config.connected, @_retryDelay + @_lagTime)
-    @connect(@_retryDelay)+    @connect(@_retryDelay)


s/scope/scopeName/

s/scope/scopeName/

--- src/main/resources/syntax-templates/atom/default.cson
+++ src/main/resources/syntax-templates/atom/default.cson
@@ -1,4 +1,4 @@
-'scope': 'source.mscript'
+'scopeName': 'source.mscript'
 'name': 'CommandHelper - MScript'
 'fileTypes': [
   'ms'


fix resetQuestRecord

fix resetQuestRecord

--- views/components/main/parts/task-panel.cjsx
+++ views/components/main/parts/task-panel.cjsx
@@ -134,10 +134,11 @@
   questRecord[id].active = false
   syncQuestRecord()
 resetQuestRecord = (types, resetInterval, id, questRecord) ->
-  if questGoals[id]? and questGoals[id].type in types
+  return unless questGoals[id]?
+  if questGoals[id].type in types
     questRecord.active = false
     questRecord.count = 0
-  else if questRecord.resetInterval is resetInterval
+  else if questGoals[id].resetInterval is resetInterval
     questRecord.count = 0
 resetQuestRecordDaily = resetQuestRecord.bind(null, [2, 4, 5], 1)
 resetQuestRecordWeekly = resetQuestRecord.bind(null, [3], 2)
@@ -206,12 +207,11 @@
           questRecord = CSON.parseCSONFile join(APPDATA_PATH, "quest_tracking_#{memberId}.cson")
           if questRecord? and questRecord.time?
             now = (new Date()).getTime()
-            isDiffDay = isDifferentDay now, prevTime
-            isDiffWeek = isDifferentWeek now, prevTime
-            isDiffMonth = isDifferentMonth now, prevTime
+            return unless isDifferentDay now, questRecord.time
+            isDiffWeek = isDifferentWeek now, questRecord.time
+            isDiffMonth = isDifferentMonth now, questRecord.time
             for id, q of questRecord when questGoals[id]?
-              if isDiffDay
-                resetQuestRecordDaily id, q
+              resetQuestRecordDaily id, q
               if isDiffWeek
                 resetQuestRecordWeekly id, q
               if isDiffMonth


only allow move for stand alone poll

only allow move for stand alone poll

--- vue/src/shared/services/poll_service.coffee
+++ vue/src/shared/services/poll_service.coffee
@@ -56,7 +56,7 @@
       name: 'common.action.move'
       icon: 'mdi-folder-swap-outline'
       canPerform: ->
-        AbilityService.canEditPoll(poll)
+        !poll.discussionId && AbilityService.canEditPoll(poll)
       perform: ->
         openModal
           component: 'PollCommonMoveForm'


Now "subject" field is hidden.

Now "subject" field is hidden.

--- client/landing/app/Applications/Inbox.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Inbox.kdapplication/AppController.coffee
@@ -81,7 +81,8 @@
                 callback      : -> modal.destroy()

     {subject, message} = modal.modalTabs.forms.sendForm.inputs
-    toField = modal.modalTabs.forms.sendForm.fields.to
+    toField            = modal.modalTabs.forms.sendForm.fields.to
+    subjectField       = modal.modalTabs.forms.sendForm.fields.subject

     recipientsWrapper = new KDView
       cssClass      : "completed-items"
@@ -112,6 +113,8 @@
         myName = KD.utils.getFullnameFromAccount()

         subject.setValue "Private message from #{myName}"
+        subjectField.hide()
+
         modal.setTitle "#{modal.getOption('title')} to <b>#{toName}</b>"

       message.$().focus()


socialworker: remove feed cache

socialworker: remove feed cache

--- workers/social/lib/social/cache/feed.coffee
+++ workers/social/lib/social/cache/feed.coffee
@@ -1,92 +0,0 @@
-{dash}  = require 'bongo'
-JTag    = require '../models/tag'
-JNewApp    = require '../models/app'
-
-localPrefetchedFeeds = {}
-
-module.exports = (options={}, callback)->
-  {bongoModels, client, intro} = options
-
-  fetchMembersFromGraph = (bongoModels, client, cb)->
-    return cb null, [] unless bongoModels
-    {JGroup}  = bongoModels
-    groupName = client?.context?.group or 'koding'
-    JGroup.one slug: groupName, (err, group)->
-      return cb null, [] if err
-      group.fetchMembersFromGraph client, {}, cb
-
-  fetchActivity = (bongoModels, client, cb)->
-    unless bongoModels
-      console.error "bongo models not found, fetch activity"
-      return cb null, []
-
-    {JNewStatusUpdate} = bongoModels
-    # options = facets : "Everything"
-
-    JNewStatusUpdate.fetchGroupActivity client, options, (err, data)->
-      if err
-        console.error "pre-fetch group activity err", err
-        return cb null, []
-
-      return cb null, data
-
-  fetchMostLikedActivity = (bongoModels, client, cb)->
-
-    unless bongoModels
-      console.error "bongo models not found"
-      return cb null, []
-
-    {JNewStatusUpdate} = bongoModels
-
-    aDay = 24 * 60 * 60 * 1000
-    mostLikedOptions =
-      sort : 'meta.likes' : -1
-      from : Date.now() - aDay
-      limit: 5
-
-    JNewStatusUpdate.fetchGroupActivity client, mostLikedOptions, (err, data)->
-      if err
-        console.error "pre-fetch most likes err:", err
-        return cb null, []
-
-      return cb null, data
-
-  # set interval options for later use
-  intervalOptions = options
-
-  queue          = []
-  defaultOptions =
-    limit : 20
-    skip  : 0
-    sort  : 'counts.followers' : -1
-
-  # queue.push ->
-  #   fetchMembersFromGraph bongoModels, client, (err, members)->
-  #     localPrefetchedFeeds['members.main'] = members  if members
-  #     queue.fin()
-
-  # Modified this function to fetch groups' tags
-  # also we can return topics for all groups
-  # queue.push ->
-  #   JTag._some client, {}, defaultOptions, (err, topics)->
-  #     localPrefetchedFeeds['topics.main'] = topics  if topics
-  #     queue.fin()
-
-  # This is not koding specific so we can return this to every group
-  # queue.push ->
-  #   JNewApp.some {"approved": true}, defaultOptions, (err, apps)->
-  #     localPrefetchedFeeds['apps.main'] = apps  if apps
-  #     queue.fin()
-
-  # we are fetching group activity, so again we can return this one for all groups
-  queue.push ->
-    fetchActivity bongoModels, client, (err, activity)=>
-      localPrefetchedFeeds['activity.main'] = activity  if activity
-      queue.fin()
-
-  queue.push ->
-    fetchMostLikedActivity bongoModels, client, (err, activity)=>
-      localPrefetchedFeeds['mostlikedactivity.main'] = activity  if activity
-      queue.fin()
-
-  dash queue, ()-> callback null, localPrefetchedFeeds


get is sabotaging speed of inner loop

get is sabotaging speed of inner loop

--- subtree/bokehjs/static/coffee/schema_renderers.coffee
+++ subtree/bokehjs/static/coffee/schema_renderers.coffee
@@ -127,7 +127,6 @@
     super()
     selected = {}
     sel_idxs = @model.get_obj('data_source').get('selected')
-    console.log('sel_idxs', sel_idxs)
     if sel_idxs.length == 0
       selecting = false
     else
@@ -147,7 +146,6 @@
     mark_type = @mget('mark')
     for idx in [0..data.length]
       if selecting and not selected[idx]
-        unselected_color = @mget('unselected_color')
         @plot_view.ctx.strokeStyle = unselected_color
         @plot_view.ctx.fillStyle = unselected_color
       else if color_field