Commit | Line | Data |
---|---|---|
e7c93dd4 | 1 | (function($, _, Backbone) { |
6a488035 TO |
2 | if (!CRM.Backbone) CRM.Backbone = {}; |
3 | ||
231a4c0f TO |
4 | /** |
5 | * Backbone.sync provider which uses CRM.api() for I/O. | |
6 | * To support CRUD operations, model classes must be defined with a "crmEntityName" property. | |
7 | * To load collections using API queries, set the "crmCriteria" property or override the | |
8 | * method "toCrmCriteria". | |
9 | * | |
4630e5b5 | 10 | * @param method Accepts normal Backbone.sync methods; also accepts "crm-replace" |
231a4c0f TO |
11 | * @param model |
12 | * @param options | |
e8b1bc2a | 13 | * @see tests/qunit/crm-backbone |
231a4c0f TO |
14 | */ |
15 | CRM.Backbone.sync = function(method, model, options) { | |
16 | var isCollection = _.isArray(model.models); | |
17 | ||
3e64165a | 18 | var apiOptions, params; |
231a4c0f | 19 | if (isCollection) { |
3e64165a | 20 | apiOptions = { |
231a4c0f TO |
21 | success: function(data) { |
22 | // unwrap data | |
23 | options.success(_.toArray(data.values)); | |
24 | }, | |
25 | error: function(data) { | |
26 | // CRM.api displays errors by default, but Backbone.sync | |
27 | // protocol requires us to override "error". This restores | |
28 | // the default behavior. | |
29 | $().crmError(data.error_message, ts('Error')); | |
30 | options.error(data); | |
31 | } | |
32 | }; | |
33 | switch (method) { | |
34 | case 'read': | |
6bc90ee8 | 35 | CRM.api(model.crmEntityName, model.toCrmAction('get'), model.toCrmCriteria(), apiOptions); |
231a4c0f | 36 | break; |
4630e5b5 TO |
37 | // replace all entities matching "x.crmCriteria" with new entities in "x.models" |
38 | case 'crm-replace': | |
3e64165a | 39 | params = this.toCrmCriteria(); |
4630e5b5 TO |
40 | params.version = 3; |
41 | params.values = this.toJSON(); | |
6bc90ee8 | 42 | CRM.api(model.crmEntityName, model.toCrmAction('replace'), params, apiOptions); |
4630e5b5 | 43 | break; |
231a4c0f TO |
44 | default: |
45 | apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"}); | |
46 | break; | |
47 | } | |
48 | } else { | |
49 | // callback options to pass to CRM.api | |
3e64165a | 50 | apiOptions = { |
231a4c0f TO |
51 | success: function(data) { |
52 | // unwrap data | |
3e64165a | 53 | var values = _.toArray(data.values); |
005f3f4d | 54 | if (data.count == 1) { |
231a4c0f TO |
55 | options.success(values[0]); |
56 | } else { | |
57 | data.is_error = 1; | |
58 | data.error_message = ts("Expected exactly one response"); | |
59 | apiOptions.error(data); | |
60 | } | |
61 | }, | |
62 | error: function(data) { | |
63 | // CRM.api displays errors by default, but Backbone.sync | |
64 | // protocol requires us to override "error". This restores | |
65 | // the default behavior. | |
66 | $().crmError(data.error_message, ts('Error')); | |
67 | options.error(data); | |
68 | } | |
69 | }; | |
70 | switch (method) { | |
71 | case 'create': // pass-through | |
72 | case 'update': | |
3e64165a TO |
73 | params = model.toJSON(); |
74 | if (!params.options) params.options = {}; | |
a0a0f1df | 75 | params.options.reload = 1; |
f0e1e747 | 76 | if (!model._isDuplicate) { |
6bc90ee8 | 77 | CRM.api(model.crmEntityName, model.toCrmAction('create'), params, apiOptions); |
f0e1e747 | 78 | } else { |
6bc90ee8 | 79 | CRM.api(model.crmEntityName, model.toCrmAction('duplicate'), params, apiOptions); |
f0e1e747 | 80 | } |
231a4c0f TO |
81 | break; |
82 | case 'read': | |
005f3f4d TO |
83 | case 'delete': |
84 | var apiAction = (method == 'delete') ? 'delete' : 'get'; | |
3e64165a | 85 | params = model.toCrmCriteria(); |
231a4c0f TO |
86 | if (!params.id) { |
87 | apiOptions.error({is_error: 1, error_message: 'Missing ID for ' + model.crmEntityName}); | |
88 | return; | |
89 | } | |
6bc90ee8 | 90 | CRM.api(model.crmEntityName, model.toCrmAction(apiAction), params, apiOptions); |
231a4c0f | 91 | break; |
231a4c0f TO |
92 | default: |
93 | apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for models"}); | |
94 | } | |
95 | } | |
96 | }; | |
97 | ||
98 | /** | |
99 | * Connect a "model" class to CiviCRM's APIv3 | |
100 | * | |
101 | * @code | |
102 | * // Setup class | |
103 | * var ContactModel = Backbone.Model.extend({}); | |
104 | * CRM.Backbone.extendModel(ContactModel, "Contact"); | |
105 | * | |
106 | * // Use class | |
107 | * c = new ContactModel({id: 3}); | |
108 | * c.fetch(); | |
109 | * @endcode | |
110 | * | |
111 | * @param Class ModelClass | |
112 | * @param string crmEntityName APIv3 entity name, such as "Contact" or "CustomField" | |
e8b1bc2a | 113 | * @see tests/qunit/crm-backbone |
231a4c0f TO |
114 | */ |
115 | CRM.Backbone.extendModel = function(ModelClass, crmEntityName) { | |
116 | // Defaults - if specified in ModelClass, preserve | |
117 | _.defaults(ModelClass.prototype, { | |
118 | crmEntityName: crmEntityName, | |
6bc90ee8 | 119 | crmActions: {}, // map: string backboneActionName => string serverSideActionName |
1518570a | 120 | crmReturn: null, // array: list of fields to return |
6bc90ee8 TO |
121 | toCrmAction: function(action) { |
122 | return this.crmActions[action] ? this.crmActions[action] : action; | |
123 | }, | |
231a4c0f | 124 | toCrmCriteria: function() { |
1518570a | 125 | var result = (this.get('id')) ? {id: this.get('id')} : {}; |
89070b40 | 126 | if (!_.isEmpty(this.crmReturn)) { |
1518570a TO |
127 | result.return = this.crmReturn; |
128 | } | |
129 | return result; | |
f0e1e747 TO |
130 | }, |
131 | duplicate: function() { | |
132 | var newModel = new ModelClass(this.toJSON()); | |
133 | newModel._isDuplicate = true; | |
134 | if (newModel.setModified) newModel.setModified(); | |
135 | newModel.listenTo(newModel, 'sync', function(){ | |
136 | // may get called on subsequent resaves -- don't care! | |
137 | delete newModel._isDuplicate; | |
138 | }); | |
139 | return newModel; | |
231a4c0f TO |
140 | } |
141 | }); | |
142 | // Overrides - if specified in ModelClass, replace | |
143 | _.extend(ModelClass.prototype, { | |
144 | sync: CRM.Backbone.sync | |
145 | }); | |
146 | }; | |
147 | ||
b488e159 TO |
148 | /** |
149 | * Configure a model class to track whether a model has unsaved changes. | |
150 | * | |
4630e5b5 TO |
151 | * Methods: |
152 | * - setModified() - flag the model as modified/dirty | |
153 | * - isSaved() - return true if there have been no changes to the data since the last fetch or save | |
154 | * Events: | |
155 | * - saved(object model, bool is_saved) - triggered whenever isSaved() value would change | |
b488e159 TO |
156 | * |
157 | * Note: You should not directly call isSaved() within the context of the success/error/sync callback; | |
158 | * I haven't found a way to make isSaved() behave correctly within these callbacks without patching | |
159 | * Backbone. Instead, attach an event listener to the 'saved' event. | |
160 | * | |
161 | * @param ModelClass | |
162 | */ | |
163 | CRM.Backbone.trackSaved = function(ModelClass) { | |
164 | // Retain references to some of the original class's functions | |
165 | var Parent = _.pick(ModelClass.prototype, 'initialize', 'save', 'fetch'); | |
166 | ||
167 | // Private callback | |
168 | var onSyncSuccess = function() { | |
169 | this._modified = false; | |
170 | if (this._oldModified.length > 0) { | |
171 | this._oldModified.pop(); | |
172 | } | |
173 | this.trigger('saved', this, this.isSaved()); | |
174 | }; | |
175 | var onSaveError = function() { | |
176 | if (this._oldModified.length > 0) { | |
177 | this._modified = this._oldModified.pop(); | |
178 | this.trigger('saved', this, this.isSaved()); | |
179 | } | |
180 | }; | |
181 | ||
182 | // Defaults - if specified in ModelClass, preserve | |
183 | _.defaults(ModelClass.prototype, { | |
184 | isSaved: function() { | |
a8ad539c | 185 | var result = !this.isNew() && !this.isModified(); |
b488e159 TO |
186 | return result; |
187 | }, | |
a8ad539c | 188 | isModified: function() { |
67aaa7a2 | 189 | return this._modified; |
a8ad539c | 190 | }, |
b488e159 TO |
191 | _saved_onchange: function(model, options) { |
192 | if (options.parse) return; | |
a8ad539c | 193 | // console.log('change', model.changedAttributes(), model.previousAttributes()); |
ce10e55a TO |
194 | this.setModified(); |
195 | }, | |
196 | setModified: function() { | |
b488e159 TO |
197 | var oldModified = this._modified; |
198 | this._modified = true; | |
199 | if (!oldModified) { | |
200 | this.trigger('saved', this, this.isSaved()); | |
201 | } | |
202 | } | |
203 | }); | |
204 | ||
205 | // Overrides - if specified in ModelClass, replace | |
206 | _.extend(ModelClass.prototype, { | |
207 | initialize: function(options) { | |
208 | this._modified = false; | |
209 | this._oldModified = []; | |
210 | this.listenTo(this, 'change', this._saved_onchange); | |
211 | this.listenTo(this, 'error', onSaveError); | |
212 | this.listenTo(this, 'sync', onSyncSuccess); | |
213 | if (Parent.initialize) { | |
214 | return Parent.initialize.apply(this, arguments); | |
215 | } | |
216 | }, | |
217 | save: function() { | |
218 | // we'll assume success | |
219 | this._oldModified.push(this._modified); | |
220 | return Parent.save.apply(this, arguments); | |
221 | }, | |
222 | fetch: function() { | |
223 | this._oldModified.push(this._modified); | |
224 | return Parent.fetch.apply(this, arguments); | |
225 | } | |
226 | }); | |
227 | }; | |
228 | ||
231a4c0f | 229 | /** |
ce10e55a TO |
230 | * Configure a model class to support client-side soft deletion. |
231 | * One can call "model.setDeleted(BOOLEAN)" to flag an entity for | |
232 | * deletion (or not) -- however, deletion will be deferred until save() | |
233 | * is called. | |
234 | * | |
4630e5b5 TO |
235 | * Methods: |
236 | * setSoftDeleted(boolean) - flag the model as deleted (or not-deleted) | |
237 | * isSoftDeleted() - determine whether model has been soft-deleted | |
ce10e55a | 238 | * Events: |
4630e5b5 | 239 | * softDelete(model, is_deleted) -- change value of is_deleted |
ce10e55a TO |
240 | * |
241 | * @param ModelClass | |
242 | */ | |
243 | CRM.Backbone.trackSoftDelete = function(ModelClass) { | |
244 | // Retain references to some of the original class's functions | |
245 | var Parent = _.pick(ModelClass.prototype, 'save'); | |
246 | ||
247 | // Defaults - if specified in ModelClass, preserve | |
248 | _.defaults(ModelClass.prototype, { | |
249 | is_soft_deleted: false, | |
250 | setSoftDeleted: function(is_deleted) { | |
251 | if (this.is_soft_deleted != is_deleted) { | |
252 | this.is_soft_deleted = is_deleted; | |
253 | this.trigger('softDelete', this, is_deleted); | |
254 | if (this.setModified) this.setModified(); // FIXME: ugly interaction, trackSoftDelete-trackSaved | |
255 | } | |
256 | }, | |
257 | isSoftDeleted: function() { | |
258 | return this.is_soft_deleted; | |
259 | } | |
260 | }); | |
261 | ||
262 | // Overrides - if specified in ModelClass, replace | |
263 | _.extend(ModelClass.prototype, { | |
264 | save: function(attributes, options) { | |
265 | if (this.isSoftDeleted()) { | |
266 | return this.destroy(options); | |
267 | } else { | |
268 | return Parent.save.apply(this, arguments); | |
269 | } | |
270 | } | |
271 | }); | |
272 | }; | |
273 | ||
274 | /** | |
231a4c0f TO |
275 | * Connect a "collection" class to CiviCRM's APIv3 |
276 | * | |
277 | * Note: the collection supports a special property, crmCriteria, which is an array of | |
4630e5b5 | 278 | * query options to send to the API. |
231a4c0f TO |
279 | * |
280 | * @code | |
281 | * // Setup class | |
282 | * var ContactModel = Backbone.Model.extend({}); | |
283 | * CRM.Backbone.extendModel(ContactModel, "Contact"); | |
284 | * var ContactCollection = Backbone.Collection.extend({ | |
285 | * model: ContactModel | |
286 | * }); | |
287 | * CRM.Backbone.extendCollection(ContactCollection); | |
288 | * | |
a449b17d | 289 | * // Use class (with passive criteria) |
231a4c0f TO |
290 | * var c = new ContactCollection([], { |
291 | * crmCriteria: {contact_type: 'Organization'} | |
292 | * }); | |
293 | * c.fetch(); | |
4630e5b5 TO |
294 | * c.get(123).set('property', 'value'); |
295 | * c.get(456).setDeleted(true); | |
296 | * c.save(); | |
a449b17d TO |
297 | * |
298 | * // Use class (with active criteria) | |
299 | * var criteriaModel = new SomeModel({ | |
300 | * contact_type: 'Organization' | |
301 | * }); | |
302 | * var c = new ContactCollection([], { | |
303 | * crmCriteriaModel: criteriaModel | |
304 | * }); | |
305 | * c.fetch(); | |
306 | * c.get(123).set('property', 'value'); | |
307 | * c.get(456).setDeleted(true); | |
308 | * c.save(); | |
231a4c0f TO |
309 | * @endcode |
310 | * | |
a449b17d | 311 | * |
231a4c0f | 312 | * @param Class CollectionClass |
e8b1bc2a | 313 | * @see tests/qunit/crm-backbone |
231a4c0f TO |
314 | */ |
315 | CRM.Backbone.extendCollection = function(CollectionClass) { | |
316 | var origInit = CollectionClass.prototype.initialize; | |
317 | // Defaults - if specified in CollectionClass, preserve | |
318 | _.defaults(CollectionClass.prototype, { | |
319 | crmEntityName: CollectionClass.prototype.model.prototype.crmEntityName, | |
6bc90ee8 TO |
320 | crmActions: {}, // map: string backboneActionName => string serverSideActionName |
321 | toCrmAction: function(action) { | |
322 | return this.crmActions[action] ? this.crmActions[action] : action; | |
323 | }, | |
231a4c0f | 324 | toCrmCriteria: function() { |
1518570a | 325 | var result = (this.crmCriteria) ? _.extend({}, this.crmCriteria) : {}; |
89070b40 | 326 | if (!_.isEmpty(this.crmReturn)) { |
1518570a | 327 | result.return = this.crmReturn; |
89070b40 | 328 | } else if (this.model && !_.isEmpty(this.model.prototype.crmReturn)) { |
1518570a TO |
329 | result.return = this.model.prototype.crmReturn; |
330 | } | |
331 | return result; | |
4630e5b5 TO |
332 | }, |
333 | ||
a449b17d TO |
334 | /** |
335 | * Get an object which represents this collection's criteria | |
336 | * as a live model. Any changes to the model will be applied | |
337 | * to the collection, and the collection will be refreshed. | |
338 | * | |
339 | * @param criteriaModelClass | |
340 | */ | |
341 | setCriteriaModel: function(criteriaModel) { | |
342 | var collection = this; | |
343 | this.crmCriteria = criteriaModel.toJSON(); | |
344 | this.listenTo(criteriaModel, 'change', function() { | |
345 | collection.crmCriteria = criteriaModel.toJSON(); | |
346 | collection.debouncedFetch(); | |
347 | }); | |
348 | }, | |
349 | ||
350 | debouncedFetch: _.debounce(function() { | |
351 | this.fetch({reset: true}); | |
e5544ed5 | 352 | }, 100), |
a449b17d | 353 | |
4630e5b5 TO |
354 | /** |
355 | * Reconcile the server's collection with the client's collection. | |
356 | * New/modified items from the client will be saved/updated on the | |
357 | * server. Deleted items from the client will be deleted on the | |
358 | * server. | |
359 | * | |
360 | * @param Object options - accepts "success" and "error" callbacks | |
361 | */ | |
362 | save: function(options) { | |
3e64165a | 363 | if (!options) options = {}; |
4630e5b5 TO |
364 | var collection = this; |
365 | var success = options.success; | |
366 | options.success = function(resp) { | |
367 | // Ensure attributes are restored during synchronous saves. | |
368 | collection.reset(resp, options); | |
369 | if (success) success(collection, resp, options); | |
370 | // collection.trigger('sync', collection, resp, options); | |
371 | }; | |
372 | wrapError(collection, options); | |
373 | ||
3e64165a | 374 | return this.sync('crm-replace', this, options); |
231a4c0f TO |
375 | } |
376 | }); | |
377 | // Overrides - if specified in CollectionClass, replace | |
378 | _.extend(CollectionClass.prototype, { | |
379 | sync: CRM.Backbone.sync, | |
380 | initialize: function(models, options) { | |
3e64165a | 381 | if (!options) options = {}; |
a449b17d TO |
382 | if (options.crmCriteriaModel) { |
383 | this.setCriteriaModel(options.crmCriteriaModel); | |
384 | } else if (options.crmCriteria) { | |
231a4c0f TO |
385 | this.crmCriteria = options.crmCriteria; |
386 | } | |
6bc90ee8 TO |
387 | if (options.crmActions) { |
388 | this.crmActions = _.extend(this.crmActions, options.crmActions); | |
389 | } | |
231a4c0f TO |
390 | if (origInit) { |
391 | return origInit.apply(this, arguments); | |
392 | } | |
4630e5b5 TO |
393 | }, |
394 | toJSON: function() { | |
395 | var result = []; | |
396 | // filter models list, excluding any soft-deleted items | |
397 | this.each(function(model) { | |
398 | // if model doesn't track soft-deletes | |
399 | // or if model tracks soft-deletes and wasn't soft-deleted | |
400 | if (!model.isSoftDeleted || !model.isSoftDeleted()) { | |
401 | result.push(model.toJSON()); | |
402 | } | |
403 | }); | |
404 | return result; | |
231a4c0f TO |
405 | } |
406 | }); | |
407 | }; | |
408 | ||
4bbb9761 TO |
409 | /** |
410 | * Find a single record, or create a new record. | |
411 | * | |
412 | * @param Object options: | |
413 | * - CollectionClass: class | |
414 | * - crmCriteria: Object values to search/default on | |
415 | * - defaults: Object values to put on newly created model (if needed) | |
416 | * - success: function(model) | |
417 | * - error: function(collection, error) | |
418 | */ | |
419 | CRM.Backbone.findCreate = function(options) { | |
3e64165a | 420 | if (!options) options = {}; |
4bbb9761 TO |
421 | var collection = new options.CollectionClass([], { |
422 | crmCriteria: options.crmCriteria | |
423 | }); | |
424 | collection.fetch({ | |
425 | success: function(collection) { | |
89070b40 | 426 | if (collection.length === 0) { |
4bbb9761 TO |
427 | var attrs = _.extend({}, collection.crmCriteria, options.defaults || {}); |
428 | var model = collection._prepareModel(attrs, options); | |
429 | options.success(model); | |
430 | } else if (collection.length == 1) { | |
431 | options.success(collection.first()); | |
432 | } else { | |
433 | options.error(collection, { | |
434 | is_error: 1, | |
435 | error_message: 'Too many matches' | |
436 | }); | |
437 | } | |
438 | }, | |
439 | error: function(collection, errorData) { | |
440 | if (options.error) { | |
441 | options.error(collection, errorData); | |
442 | } | |
443 | } | |
444 | }); | |
445 | }; | |
446 | ||
447 | ||
6a488035 TO |
448 | CRM.Backbone.Model = Backbone.Model.extend({ |
449 | /** | |
450 | * Return JSON version of model -- but only include fields that are | |
451 | * listed in the 'schema'. | |
452 | * | |
453 | * @return {*} | |
454 | */ | |
455 | toStrictJSON: function() { | |
456 | var schema = this.schema; | |
457 | var result = this.toJSON(); | |
e8b1bc2a TO |
458 | _.each(result, function(value, key) { |
459 | if (!schema[key]) { | |
6a488035 TO |
460 | delete result[key]; |
461 | } | |
462 | }); | |
463 | return result; | |
464 | }, | |
465 | setRel: function(key, value, options) { | |
466 | this.rels = this.rels || {}; | |
467 | if (this.rels[key] != value) { | |
468 | this.rels[key] = value; | |
e8b1bc2a | 469 | this.trigger("rel:" + key, value); |
6a488035 TO |
470 | } |
471 | }, | |
472 | getRel: function(key) { | |
473 | return this.rels ? this.rels[key] : null; | |
474 | } | |
475 | }); | |
476 | ||
477 | CRM.Backbone.Collection = Backbone.Collection.extend({ | |
478 | /** | |
479 | * Store 'key' on this.rel and automatically copy it to | |
480 | * any children. | |
481 | * | |
482 | * @param key | |
483 | * @param value | |
484 | * @param initialModels | |
485 | */ | |
486 | initializeCopyToChildrenRelation: function(key, value, initialModels) { | |
487 | this.setRel(key, value, {silent: true}); | |
488 | this.on('reset', this._copyToChildren, this); | |
489 | this.on('add', this._copyToChild, this); | |
490 | }, | |
491 | _copyToChildren: function() { | |
492 | var collection = this; | |
e8b1bc2a | 493 | collection.each(function(model) { |
6a488035 TO |
494 | collection._copyToChild(model); |
495 | }); | |
496 | }, | |
497 | _copyToChild: function(model) { | |
e8b1bc2a | 498 | _.each(this.rels, function(relValue, relKey) { |
6a488035 TO |
499 | model.setRel(relKey, relValue, {silent: true}); |
500 | }); | |
501 | }, | |
502 | setRel: function(key, value, options) { | |
503 | this.rels = this.rels || {}; | |
504 | if (this.rels[key] != value) { | |
505 | this.rels[key] = value; | |
e8b1bc2a | 506 | this.trigger("rel:" + key, value); |
6a488035 TO |
507 | } |
508 | }, | |
509 | getRel: function(key) { | |
510 | return this.rels ? this.rels[key] : null; | |
511 | } | |
512 | }); | |
513 | ||
514 | /* | |
515 | CRM.Backbone.Form = Backbone.Form.extend({ | |
516 | validate: function() { | |
517 | // Add support for form-level validators | |
518 | var errors = Backbone.Form.prototype.validate.apply(this, []) || {}; | |
519 | var self = this; | |
520 | if (this.validators) { | |
521 | _.each(this.validators, function(validator) { | |
522 | var modelErrors = validator(this.getValue()); | |
523 | ||
524 | // The following if() has been copied-pasted from the parent's | |
525 | // handling of model-validators. They are similar in that the errors are | |
526 | // probably keyed by field names... but not necessarily, so we use _others | |
527 | // as a fallback. | |
528 | if (modelErrors) { | |
529 | var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors); | |
530 | ||
531 | //If errors are not in object form then just store on the error object | |
532 | if (!isDictionary) { | |
533 | errors._others = errors._others || []; | |
534 | errors._others.push(modelErrors); | |
535 | } | |
536 | ||
537 | //Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' }) | |
538 | if (isDictionary) { | |
539 | _.each(modelErrors, function(val, key) { | |
540 | //Set error on field if there isn't one already | |
541 | if (self.fields[key] && !errors[key]) { | |
542 | self.fields[key].setError(val); | |
543 | errors[key] = val; | |
544 | } | |
545 | ||
546 | else { | |
547 | //Otherwise add to '_others' key | |
548 | errors._others = errors._others || []; | |
549 | var tmpErr = {}; | |
550 | tmpErr[key] = val; | |
551 | errors._others.push(tmpErr); | |
552 | } | |
553 | }); | |
554 | } | |
555 | } | |
556 | ||
557 | }); | |
558 | } | |
559 | return _.isEmpty(errors) ? null : errors; | |
560 | } | |
561 | }); | |
562 | */ | |
4630e5b5 TO |
563 | |
564 | // Wrap an optional error callback with a fallback error event. | |
565 | var wrapError = function (model, options) { | |
566 | var error = options.error; | |
567 | options.error = function(resp) { | |
3e64165a | 568 | if (error) error(model, resp, optio); |
4630e5b5 TO |
569 | model.trigger('error', model, resp, options); |
570 | }; | |
571 | }; | |
e7c93dd4 | 572 | })(CRM.$, CRM._, CRM.BB); |