Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add error callback for raw data #94

Merged
merged 23 commits into from

2 participants

@dianatatu
Collaborator

Need

Besides success callback, we need to have an error callback for errors in raw data.
This is currently needed in #7849 .

Solution

  • add erorr callback that triggers an error event.

Files changed

  • thehole/app/core/raw_data.coffee

cc @aismail

@dianatatu
Collaborator

@aismail please review :)

core/raw_data.coffee
@@ -149,12 +149,17 @@ define [], () ->
# Trigger a no_data event when the response is empty
if _.isEmpty(data)
@trigger('no_data', @)
+
+ error_callback = (xhr, response_status, error_string) =>
+ @trigger('error', @, @, xhr.status)
@skidding Owner

This https://github.com/uberVU/mozaic/blob/master/core/libs/backbone/backbone-0.9.1.js#L323 is what we're trying to emulate from the Backbone model, right? This is the event triggered by the wrapError method it leads to: https://github.com/uberVU/mozaic/blob/master/core/libs/backbone/backbone-0.9.1.js#L1341 . Does the 'error', originalModel, resp, options event signature match yours 'error', @, @, xhr.status?

I don't think we should have "this" twice, here's an example of what we expect from the contents of the error event: https://github.com/uberVU/mozaic/blob/master/core/widget/backbone_events.coffee#L35-L45

Also, we use this instead of just @ now, when we don't have any leading property. E.g. trigger(this) or trigger(@property). You can replace it elsewhere in the file as well, if you want to do it

@dianatatu Collaborator
  1. The call matches the trigger event signature
  2. params[0] expects the model and params[1] the collection. I have changed the returned collection on error event to be the given collection if model has no collection
  3. Thanks for the tip :) i've changed @ with this :)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@skidding
Owner

Done, LGTM. Some of us made changes on the thehole repo w/out syncing, and others viceversa. Either way, this one is all set now, after merging please run the sync_mozaic script again and create a pull request in thehole to update Mozaic in our repo. Besides the constants.coffee file, all should be migrated back.

@dianatatu dianatatu merged commit bec72be into master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 20, 2013
  1. @dianatatu
  2. @dianatatu
  3. @dianatatu
  4. @dianatatu

    Use this instead of @. #94

    dianatatu authored
  5. @dianatatu
  6. @dianatatu
  7. @dianatatu
  8. @dianatatu
  9. @dianatatu
  10. @dianatatu
  11. @dianatatu
  12. @dianatatu

    Update Mozaic. #94

    dianatatu authored
  13. @skidding
  14. @skidding
  15. @skidding
  16. @skidding

    Add more newlines to files #94

    skidding authored
  17. @skidding
  18. @skidding

    Remove sameArray method from misc #94

    skidding authored
    _.isEqual does the same
  19. @skidding
  20. @skidding

    Add back widget docstring #94

    skidding authored
  21. @skidding

    Add newlines to modules #94

    skidding authored
  22. @skidding
  23. @skidding

    Add newline to api mock file #94

    skidding authored
This page is out of date. Refresh to see the latest.
View
26 core/api_mock.coffee
@@ -5,17 +5,27 @@ define ['cs!tests/factories/master_factory'], (MasterFactory) ->
master_factory = new MasterFactory()
Methods =
+
+ # Property exposes MasterFactory's mapping between channels
+ # and factories.
+ channelsToFactoriesMapping: \
+ master_factory.getChannelsToFactoriesMapping()
+
apiMock: (resources) ->
###
Mock some api calls.
###
result = {}
+ if not @mocks
+ @mocks = {}
for resource, params of resources
if params.response
response = params.response
else
response = @getMockedApiResponse(resource, params)
- @mockResource(resource, response, params)
+ id = @mockResource(resource, response, params)
+ # Save the id to be able to clear it later and rebind it.
+ @mocks[resource] = id
# Depending on the channel type (relational or api), populate
# the response object the same way as the objects would be
@@ -27,8 +37,13 @@ define ['cs!tests/factories/master_factory'], (MasterFactory) ->
return result
mockResource: (resource, response, params = {}) ->
+ # The mockjax_options allow you to pass extra params
+ # to the mockjax configuration.
+ # Example of use case:
+ # you want the status to be 403, not 200 which is by
+ # default.
$.mockjax(
- _.extend({}, params,
+ _.extend({}, params.mockjax_options,
url: @getResourceRegExp(resource)
response: ->
@responseText = response
@@ -39,11 +54,11 @@ define ['cs!tests/factories/master_factory'], (MasterFactory) ->
endpoint = "#{App.general.FRONTAPI_URL}/.*/#{resource}/([^a-z]|$)"
return new RegExp(endpoint)
- getMockedApiResponse: (resource, param) ->
+ getMockedApiResponse: (resource, params) ->
###
Mock some API response
###
- response = master_factory.get(resource, param)
+ response = master_factory.get(resource, params)
# If is_api channel do not wrap the response under 'objects' key
# Useful for calls to analytics
# Api responses should be returned directly, w/out being wrapped
@@ -54,3 +69,6 @@ define ['cs!tests/factories/master_factory'], (MasterFactory) ->
return response[0]
else
return {'objects': response}
+
+ clearResource: (resource) ->
+ $.mockjaxClear(@mocks[resource])
View
235 core/base_widgets/list.coffee
@@ -1,6 +1,6 @@
define ['cs!scrollable_widget'], (ScrollableWidget) ->
- class WidgetList extends ScrollableWidget
+ class List extends ScrollableWidget
###
Widget which is able to render a list of items, by injecting
one widget per each item. An item to be displayed will be named
@@ -38,6 +38,13 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
fetching the list of all books from the server, and I want
to display only the books by a certain author.
+ - while the list is built around an /items channel in the
+ default implementation, its interface allows having more than
+ one channel (using aggregated channels) for building the list
+ items. All channel events must be funneled into the
+ @handleChannelEvents method, and items are joined together
+ under the @getModelsFromChannelData method
+
IMO lifecycle management of the widgets is the most important
feature of this widget - if I want to display a list, I can
concentrate on writing the widget for the list's item instead of
@@ -52,10 +59,10 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
params_defaults:
enable_scroll: 'data-params'
- className: 'data-params'
item: 'data-params'
item_channels: 'data-params'
item_params: 'data-params'
+ item_class: 'data-params'
# Used to specify what fields to dynamically extract
# from model. Should look like {key1: new_key1, key2: new_key2, ..}
# where key is the one in model, and new_key is the one will be used
@@ -64,14 +71,12 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
filter_by: 'data-params'
sort_by: 'data-params'
container: 'data-params'
- item_element: 'data-params'
+ item_element: (value) -> value or 'li'
prepend: 'data-params'
# This mappings params is used to be able
# to insert different widgets (with different
# models).
mappings: 'data-params'
- first_class: 'data-params'
- last_class: 'data-params'
# A list of comparators for list. You can also push to
# this list and define your own comparator in your type
@@ -101,7 +106,9 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
# If the scroll is enabled in the params, then make
# the items channel a scrollable one
if @enable_scroll
- @scrollable_channels = ['/items']
+ # Make all loading channels scrollable because widget states
+ # are triggered by loading ones and affect the scrollable ones
+ @scrollable_channels = _.clone(@loading_channels)
@extendWidgetParamsFromItem()
@@ -125,19 +132,15 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
# Initialize the array of IDs
@ids = []
- # Initialize first_class and last_class
- if not @first_class?
- @first_class = 'first'
- if not @last_class?
- @last_class = 'last'
-
super()
- changeState: (state, item_params) ->
- super(state, item_params)
+ changeState: (state, params...) ->
+ super(arguments...)
if state == 'empty'
# TODO: Add `end` state to widget base
- if not item_params.collection.length
+ # Support multiple loading channels for building the list's
+ # item feed
+ if not @getModelsFromChannelData(params...).length
@renderLayout {state: state}
else
# Maybe add something to the bottom of all items instead
@@ -248,48 +251,83 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
return true
get_items: (item_params) =>
+ @handleChannelEvents(item_params)
- if item_params.type == 'reset' and not @isChannelDataEmpty(item_params)
+ handleChannelEvents: (item_params) ->
+ ###
+ Method for processing channel data and inserting received items
+ onto the list. It is called w/ the single /items channel from
+ its channel callback method in the default implementation, but
+ can be used in subclasses to have additional channels that
+ construct the list's items. The items from more than one
+ channel can be aggregated using the @getModelsFromChannelData
+ method, which receives all arguments that this method received,
+ which can be a list of channel events
+ ###
+ model = item_params.model
+ models = @getModelsFromChannelData(arguments...)
+
+ # Detecting whether we need to re-render the entire list is tricky,
+ # we need a reset event on at least one channel, and either reset
+ # or no_data on the others.
+ events = _.pluck(arguments, 'type')
+ isResetEvent = 'reset' in events and
+ _.difference(events, ['reset', 'no_data']).length is 0
+ if isResetEvent and models.length
# Render layout with "available" state flag
@renderLayout {state: 'available'}
- @ids = []
- # Insert each item one by one
- item_params.collection.each( (model) =>
- if @matchesFilters(model)
- @insertItem(model, item_params.collection)
- )
+ @insertItems(models)
# Add a new item to a list by injecting a widget to the end of it
else if item_params.type == 'add'
# Clear template blank state if previous state was "empty"
# and no _reset_ event has been triggered in the meantime
- if @data_state == 'empty'
+ # The "empty" state is also triggered at the end of scrolling,
+ # so we need to check that there were no previous items before
+ # emptying the template
+ if @data_state is 'empty' and models.length <= 1
@renderLayout {state: 'available'}
- unless item_params.model.id?
- item_params.model.set('id', Utils.guid('new'))
- if @matchesFilters(item_params.model)
- @insertItem(item_params.model, item_params.collection)
+ # Create a dummy model id if missing. This can only happen if a
+ # list is hosting client-side volatile data that is not synced
+ # with a database (by adding to channel with sync: false)
+ unless model.id?
+ # Prevent from triggering a change/change_attribute event
+ # as well, by setting with silent: true
+ model.set('id', Utils.guid('new'), silent: true)
+ if @matchesFilters(model)
+ @insertItem(model, models, isLoadedLater: true)
# If the event is `change_attribute`, check if the model matches the
# filters and decide whether to add it or not
else if item_params.type == 'change_attribute'
- if @matchesFilters(item_params.model)
+ if @matchesFilters(model)
# If we have a change in one of the sort_by attributes, we
# need to remove this item and add it again, to go through
# all the insert-in-sorted-place logic. And will be added
# by the right below code again.
if item_params.attribute in @getSortByFields()
- @deleteItem(item_params.model, item_params.collection)
+ @deleteItem(model)
# Don't create duplicates and add it only if it is unique
- if @el.find(".item-#{item_params.model.id}").length == 0
- @insertItem(item_params.model, item_params.collection)
+ if @view.$el.find(".item-#{model.id}").length == 0
+ @insertItem(model, models)
# Delete a specific item from a list
else if item_params.type == 'remove'
- if @matchesFilters(item_params.model)
- @deleteItem(item_params.model, item_params.collection)
+ if @matchesFilters(model)
+ @deleteItem(model)
- insertItem: (model, collection, options = {}) =>
+ insertItems: (models) ->
+ ###
+ Batch insert, for adding more models at the same time on
+ 'reset' events. Useful for subclasses that might aggregate more
+ than one channel to build its list items
+ ###
+ @ids = []
+ # Insert each item one by one
+ _.each models, (model) =>
+ @insertItem(model, models) if @matchesFilters(model)
+
+ insertItem: (model, models, options = {}) =>
###
@param {Object} options
@param {Boolean} [options.isLoadedLater] - mark the item as being
@@ -298,61 +336,63 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
_.defaults options,
isLoadedLater: false
- @removeFirstAndLastCSS(collection)
- @_insertItem(model, collection, options)
- @addFirstAndLastCSS(collection)
-
- _insertItem: (model, collection, options) =>
# Find the name of the widget to insert.
item = @getItemWidgetName model
# Skip the insertion if widget not found
return unless item?
# Extra params of the widget.
extra_params = _.pick(options, 'isLoadedLater')
- item_widget_params = @getItemWidgetParams model, collection, extra_params
+ item_widget_params = @getItemWidgetParams(model, extra_params)
# Also add a class to uniquely identify the item
# Needed when deleting the item from the list
- class_name = if !@className then "item-#{model.id}" else "#{@className} item-#{model.id}"
+ item_class = "item-#{model.id}"
+ item_class += " #{@item_class}" if @item_class?
+
+ injectOptions =
+ params: item_widget_params
+ container: @view.$el
+ type: @item_element
+ classes: item_class
if @sort_by?
+ firstModel = @_findItemById(models, _.first(@ids))
+ lastModel = @_findItemById(models, _.last(@ids))
+
# No element so far means that the insertion is straight forward
if @ids.length == 0
- Utils.injectWidget(@el, item, item_widget_params, class_name, null, @item_element ? 'li')
@ids.push(model.id)
- return
# Otherwise, see where in the list we can insert it
# See if we must insert it before the first model
- first_model_so_far = collection.get(_.first(@ids))
- if @compare(model, first_model_so_far) <= 0
- Utils.injectWidget(@el, item, item_widget_params, class_name, null, @item_element ? 'li', false, true)
+ else if @compare(model, firstModel) <= 0
@ids.unshift(model.id)
- return
+ injectOptions.placement = 'prepend'
# See if we must insert it after the last model
- last_model_so_far = collection.get(_.last(@ids))
- if @compare(model, last_model_so_far) > 0
- Utils.injectWidget(@el, item, item_widget_params, class_name, null, @item_element ? 'li')
+ else if @compare(model, lastModel) > 0
@ids.push(model.id)
- return
# Otherwise, we're inserting it somewhere in the middle
# Also, it means that we have at least two elements in
# @ids (because if there is only one, the element e will
# either be <= it and be inserted before, or be > it and
# be inserted after).
- for i in [0..@ids.length-2]
- cur_model = collection.get(@ids[i])
- next_model = collection.get(@ids[i+1])
- if @compare(cur_model, model) < 0 and @compare(model, next_model) <= 0
- @ids.splice(i + 1, 0, model.id)
- dom_element = @el.find(".item-#{next_model.id}")
- # Insert before next_model's DOM element
- Utils.injectWidget(dom_element, item, item_widget_params, class_name, null, @item_element ? 'li', false, @prepend, true)
- return
- else
- Utils.injectWidget(@el, item, item_widget_params, class_name, null, @item_element ? 'li', false, @prepend)
+ else
+ for i in [0..@ids.length-2]
+ cur_model = @_findItemById(models, @ids[i])
+ next_model = @_findItemById(models, @ids[i + 1])
+ if @compare(cur_model, model) < 0 and
+ @compare(model, next_model) <= 0
+ @ids.splice(i + 1, 0, model.id)
+ # Insert before next_model's DOM element
+ injectOptions.container =
+ @view.$el.find(".item-#{next_model.id}")
+ injectOptions.placement = 'before'
+ break
+
+ Utils.inject(item, injectOptions)
+ @updateFirstAndLastDOMClasses()
getSortByFields: ->
fields = []
@@ -363,12 +403,7 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
fields.push(field)
fields
- deleteItem: (model, collection) =>
- @removeFirstAndLastCSS(collection)
- @_deleteItem(model, collection)
- @addFirstAndLastCSS(collection)
-
- _deleteItem: (model, collection) =>
+ deleteItem: (model) =>
###
Deletes a specific item from a list
###
@@ -377,44 +412,32 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
@ids.splice(idx, 1)
# Remove the DOM element
- @el.find(".item-#{model.id}").remove()
+ @view.$el.find(".item-#{model.id}").remove()
- addFirstAndLastCSS: (collection) =>
- ###
- Add CSS classes to the first and last items in the collection.
-
- In order to find out which are the first and last items,
- we will use @ids, which contains the current sorted state
- of the models.
- ###
- if @ids.length == 0
- return
- first_id = @ids[0]
- last_id = @ids[@ids.length - 1]
- @el.find(".item-#{first_id}").addClass(@first_class)
- @el.find(".item-#{last_id}").addClass(@last_class)
+ @updateFirstAndLastDOMClasses()
- removeFirstAndLastCSS: (collection) =>
+ updateFirstAndLastDOMClasses: ->
###
- Remove first and last CSS classes to items in the collectoin.
-
- In order to find out which are the first and last items,
- we will use @ids, which contains the current sorted state
- of the models.
+ Make sure the first and the last items of the list have a
+ 'first' and 'last' class, respectively. This is useful for
+ older browser which do not support :first-child and :last-child
+ CSS selectors but also for cases where list items might have
+ neighbors of other types inside the list wrapper (in a
+ hypothetical list subclass scenario)
###
- if @ids.length == 0
- return
- first_id = @ids[0]
- last_id = @ids[@ids.length - 1]
- @el.find(".item-#{first_id}").removeClass(@first_class)
- @el.find(".item-#{last_id}").removeClass(@last_class)
-
- getItemWidgetParams: (model, collection, extra_params) =>
+ # Only select first-level widgets (which means a list should never
+ # have inner wrappers between its view element and its item widgets)
+ $items = @view.$el.children('.mozaic-widget')
+ # Clear all items of first/last classes first
+ $items.removeClass('first').removeClass('last')
+ $items.first().addClass('first')
+ $items.last().addClass('last')
+
+ getItemWidgetParams: (model, extra_params) =>
###
Method returns the params of the item widget which will be inserted as part of the current list.
Can be overridden in specialiazed list classes to provide custom params to list items.
@param {Object} model Backbone.Model instance of the item to be inserted
- @param {Object} collection Backbone.Collection instance of all the items in the list
@param {Object} extra_params - extra params to be passed to item widgets
@return {Object}
###
@@ -456,4 +479,18 @@ define ['cs!scrollable_widget'], (ScrollableWidget) ->
###
_.extend({}, @widget_params, @item_params) if @item_params?
- return WidgetList
+ getModelsFromChannelData: (item_params) ->
+ ###
+ Get models from one or more channel events. The default list
+ implementation only uses an /items channel to draw its items
+ from
+ ###
+ return item_params.collection?.models or []
+
+ _findItemById: (list, id) ->
+ ###
+ Find an item from a sorted list, by id
+ Note: _.findWhere(list, id: id) can be used once we update to
+ Underscore 1.4.4: http://underscorejs.org/#findWhere
+ ###
+ return _.find(list, (item) -> item.id is id)
View
7 core/base_widgets/mediator_widget.coffee
@@ -21,6 +21,10 @@ define ['cs!channels_utils', 'cs!widget'], (channels_utils, Widget) ->
that receivs a list of channels as a parameter).
Default value: 'refresh'.
+ The Mediator widget also supports a list of fixed attributes for
+ the output channels, which should always be present regardless of
+ what the input channels trigger.
+
The Mediator widget also supports a list of ignored attributes for
the input channels, which changed alone shouldn't trigger any
changes on the output channels. The ignored attributes are also
@@ -38,6 +42,7 @@ define ['cs!channels_utils', 'cs!widget'], (channels_utils, Widget) ->
'output_channel': 'data-params'
'output_channels': 'data-params'
'skip_first': 'data-params'
+ 'fixed_attributes': (attrs) -> attrs or {}
'ignored_attributes': (attrs) -> attrs or []
initialize: =>
@@ -103,7 +108,7 @@ define ['cs!channels_utils', 'cs!widget'], (channels_utils, Widget) ->
skipStreampollBuffer = if options.skipStreampollBuffer? then options.skipStreampollBuffer else false
# Merge data input channels data into a single object.
- translated_channel_params = {}
+ translated_channel_params = _.clone(@fixed_attributes)
for channel_params in params
_.extend(translated_channel_params, @translateParams(channel_params))
View
26 core/base_widgets/notifications.coffee
@@ -10,11 +10,16 @@ define ['cs!widget'], (Widget) ->
_type_ and _message_, both strings.
###
loadingWidgets: 0
+ pendingNotifications: []
initialize: =>
# Subscribe to all notifications
pipe = loader.get_module('pubsub')
pipe.subscribe('/notifications', @get_notifications)
+ # Wait for the loading animation to finish in order to release
+ # pending notifications that were triggered while loading
+ pipe.subscribe('/loading_animation_finished',
+ @loadingAnimationFinished)
get_notifications: (params) =>
# Remove ALL notifications
@@ -38,7 +43,26 @@ define ['cs!widget'], (Widget) ->
if params.type is 'loading'
return if ++@loadingWidgets > 1
- @injectNotification(params)
+ # Only display notifications when the application finished loading,
+ # otherwise stack them and display them after it does.
+ # Note: Loading notifications shouldn't be delayed because they
+ # need to be canceled synchrounously and they don't reveal any
+ # actual information that the user might miss anyway
+ if App.isLoading and params.type isnt 'loading'
+ @pendingNotifications.push(params)
+ else
+ @injectNotification(params)
+
+ loadingAnimationFinished: =>
+ ###
+ Callback for when the global loading animation finishes, where
+ all pending notifications that were triggered while the app was
+ loading should be released
+ ###
+ while @pendingNotifications.length
+ # Release notifications in the order they were added, from
+ # first to last
+ @injectNotification(@pendingNotifications.shift())
injectNotification: (params) =>
# Avoid duplicates
View
24 core/base_widgets/widget_editor.coffee
@@ -61,6 +61,7 @@ define [], () ->
###
@pipe = loader.get_module('pubsub')
@pipe.subscribe('/new_widget', @newWidget)
+ @subscribed_to_widget_rendering = false
# Compile and render widget container template
template = Handlebars.compile(@template)
@@ -88,9 +89,28 @@ define [], () ->
# Create a back-reference to editor in widget
@widget.editor = this
# Call initializeWithForm method on editor, if one is
- # implemented
+ # implemented. Delay this call until renderLayout() has
+ # been called at least once, because most widget editors
+ # do setValue() in initializeWithForm, which implicitly
+ # requires the DOM to be ready.
if _.isFunction(@widget.initializeWithForm)
- @widget.initializeWithForm(@form)
+ if @widget.DELAYED_INITIALIZE_WITH_FORM? and @widget.DELAYED_INITIALIZE_WITH_FORM
+ @pipe.subscribe('/new_widget_rendered', @editorWidgetHasRendered)
+ else
+ @widget.initializeWithForm(@form)
+
+ editorWidgetHasRendered: (widget_id, widget_name) =>
+ ###
+ Callback for running initialize-like method of widget editor,
+ that is called whenever the proxy widget and the editor itself
+ are setup and cross-referenced correctly.
+
+ Since most editor widgets also use the DOM in initializeWithForm,
+ we make sure that the DOM is right there by waiting for at least
+ one renderLayout() to occur.
+ ###
+ @widget.initializeWithForm(@form)
+ @pipe.unsubscribe('/new_widget_rendered', @editorWidgetHasRendered)
getValue: () ->
return if @widget? then @widget.getValue() else @default_value
View
5 core/loading_animation.coffee
@@ -35,6 +35,8 @@ define ['cs!mozaic_module'], (Module) ->
@pipe.subscribe('/new_widget_rendered', @newWidgetRendered)
@start_time = new Date().getTime()
@intervalHandle = setInterval(@checkIfLoadingFinished, @LOADING_FINISHED_CHECK_INTERVAL)
+ # Mark the entire application as "loading"
+ App.isLoading = true
# If the admin changed the group, personalize
# loading message.
@@ -125,6 +127,9 @@ define ['cs!mozaic_module'], (Module) ->
###
$('#loading-animation').hide()
clearInterval(@intervalHandle)
+ # Update the global "loading" flag from the application once the
+ # loading is done
+ App.isLoading = false
@pipe.publish('/loading_animation_finished')
getNastyWidgets: =>
View
9 core/raw_data.coffee
@@ -149,12 +149,21 @@ define [], () ->
# Trigger a no_data event when the response is empty
if _.isEmpty(data)
@trigger('no_data', @)
+
+ error_callback = (xhr, response_status, error_string) =>
+ # Trigger an 'error' event when the request results in an error
+ # (including 3xx redirect and excluding 304 timeout)
+ # This is important because we need to have a response from
+ # widgets on error events.
+ @trigger('error', this, this, xhr.status)
+
# Make the actual AJAX request
call_params =
url: @url
dataType: 'json'
data: params
success: success_callback
+ error: error_callback
type: options.type || 'GET'
# If we are passed contentType, pass it through
View
2  core/scrollable_widget.coffee
@@ -30,7 +30,7 @@ define ['cs!widget'], (Widget) ->
onScroll: =>
dif = $(window).scrollTop() - $(document).height() + $(window).height()
- if Math.abs(dif) < 5
+ if Math.abs(dif) < 5 and $(document).height() > $(window).height()
@scrollDown()
return false
View
3  core/utils.coffee
@@ -37,6 +37,7 @@ define ['cs!utils/urls', 'cs!utils/time', 'cs!utils/dom', 'cs!utils/images', 'cs
@param (object) options Extra options for injecting the widgets,
here are the available ones:
- (string) id The id attribute
+ - (string) type of DOM element to inject
- (string) classes Extra set of classes
- (object) data Extra set of data attributes (the `widget`
and `params` keys are reserved for the widget
@@ -529,6 +530,8 @@ define ['cs!utils/urls', 'cs!utils/time', 'cs!utils/dom', 'cs!utils/images', 'cs
data.params = options.params or {}
node = @buildDomElement(type, id, classes, data)
+ getControllerContainer: -> $('#controller-container')
+
# Extend Utils with other utils functions (see utils/ dir) in order
# to keep the same Utils.method() interface.
_.extend(Utils, Urls)
View
2  core/widget/backbone_events.coffee
@@ -40,7 +40,7 @@ define ['cs!channels_utils'], (channels_utils) ->
return {
type: 'error'
model: params[0]
- collection: params[0].collection
+ collection: params[0].collection or params[1]
response: params[2]
}
else if event_type == 'change'
View
17 core/widget/widget.coffee
@@ -185,7 +185,7 @@ define [
@constructed_at = Utils.now() / 1000
if template
@template = template
- if params.template_name
+ if params.template_name?
@template_name = params.template_name
@params = params
@channel_mapping = params.channels or {}
@@ -257,7 +257,22 @@ define [
}
pipe = loader.get_module('pubsub')
+
+ # Announcing that a new widget is available is done in 2 waves:
+ # - first announce the people interested in the fact that the
+ # widget has appeared that will count this (for example,
+ # loading animation)
+ # - then announce the datasource to bind this widget to its
+ # channels.
+ #
+ # Doing the two publishes instead of one is extremely unfortunate
+ # because for widget editors, one can encounter aggregated
+ # channels events before the widget.editor is set by the
+ # widget_editor.
+ #
+ # TODO(andrei): find better way to fix this. Complete braindamage.
pipe.publish('/new_widget', message)
+ pipe.publish('/new_widget_bind_to_data', message)
# If this widget doesn't have a template, it either:
# a) doesn't have any visible representation
Something went wrong with that request. Please try again.