Merge pull request #13893 from colemanw/dev/core#821
[civicrm-core.git] / js / model / crm.uf.js
CommitLineData
4b513f23 1(function($, _) {
6a488035
TO
2 if (!CRM.UF) CRM.UF = {};
3
4 var YESNO = [
5 {val: 0, label: ts('No')},
6 {val: 1, label: ts('Yes')}
7 ];
8
9 var VISIBILITY = [
10 {val: 'User and User Admin Only', label: ts('User and User Admin Only'), isInSelectorAllowed: false},
fac01cb1
DG
11 {val: 'Public Pages', label: ts('Expose Publicly'), isInSelectorAllowed: true},
12 {val: 'Public Pages and Listings', label: ts('Expose Publicly and for Listings'), isInSelectorAllowed: true}
6a488035
TO
13 ];
14
15 var LOCATION_TYPES = _.map(CRM.PseudoConstant.locationType, function(value, key) {
16 return {val: key, label: value};
17 });
18 LOCATION_TYPES.unshift({val: '', label: ts('Primary')});
19 var DEFAULT_LOCATION_TYPE_ID = '';
20
21 var PHONE_TYPES = _.map(CRM.PseudoConstant.phoneType, function(value, key) {
22 return {val: key, label: value};
23 });
1a228b4c 24
25 var WEBSITE_TYPES = _.map(CRM.PseudoConstant.websiteType, function(value, key) {
26 return {val: key, label: value};
27 });
6a488035 28 var DEFAULT_PHONE_TYPE_ID = PHONE_TYPES[0].val;
1a228b4c 29 var DEFAULT_WEBSITE_TYPE_ID = WEBSITE_TYPES[0].val;
6a488035
TO
30
31 /**
32 * Add a help link to a form label
33 */
34 function addHelp(title, options) {
c7915cc9 35 return title + ' <a href="#" onclick=\'CRM.help("' + title + '", ' + JSON.stringify(options) + '); return false;\' title="' + ts('%1 Help', {1: title}) + '" aria-label="' + ts('%1 Help', {1: title}) + '" class="helpicon"></a>';
6a488035
TO
36 }
37
38 function watchChanges() {
39 CRM.designerApp.vent.trigger('ufUnsaved', true);
40 }
41
42 /**
43 * Parse a "group_type" expression
44 *
45 * @param string groupTypeExpr example: "Individual,Activity\0ActivityType:2:28"
46 * Note: I've seen problems where HTML "&#00;" != JS '\0', so we support ';;' as an equivalent delimiter
47 * @return Object example: {coreTypes: {"Individual":true,"Activity":true}, subTypes: {"ActivityType":{2: true, 28:true}]}}
48 */
49 CRM.UF.parseTypeList = function(groupTypeExpr) {
50 var typeList = {coreTypes: {}, subTypes:{}};
51 // The API may have automatically converted a string with '\0' to an array
52 var parts = _.isArray(groupTypeExpr) ? groupTypeExpr : groupTypeExpr.replace(';;','\0').split('\0');
53 var coreTypesExpr = parts[0];
54 var subTypesExpr = parts[1];
55
fb079bbf 56 if (!_.isEmpty(coreTypesExpr)) {
6a488035
TO
57 _.each(coreTypesExpr.split(','), function(coreType){
58 typeList.coreTypes[coreType] = true;
59 });
60 }
61
99e239bc 62 //CRM-15427 Allow Multiple subtype filtering
fb079bbf 63 if (!_.isEmpty(subTypesExpr)) {
99e239bc 64 if (subTypesExpr.indexOf(';;') !== -1) {
65 var subTypeparts = subTypesExpr.replace(/;;/g,'\0').split('\0');
66 _.each(subTypeparts, function(subTypepart) {
67 var subTypes = subTypepart.split(':');
68 var subTypeKey = subTypes.shift();
69 typeList.subTypes[subTypeKey] = {};
70 _.each(subTypes, function(subTypeId) {
71 typeList.subTypes[subTypeKey][subTypeId] = true;
72 });
73 });
74 }
75 else {
76 var subTypes = subTypesExpr.split(':');
77 var subTypeKey = subTypes.shift();
78 typeList.subTypes[subTypeKey] = {};
79 _.each(subTypes, function(subTypeId) {
80 typeList.subTypes[subTypeKey][subTypeId] = true;
81 });
82 }
6a488035
TO
83 }
84 return typeList;
85 };
86
87 /**
88 * This function is a hack for generating simulated values of "entity_name"
89 * in the form-field model.
90 *
91 * @param {string} field_type
92 * @return {string}
93 */
94 CRM.UF.guessEntityName = function(field_type) {
95 switch (field_type) {
96 case 'Contact':
13e4fbdd 97 case 'Individual':
bf86535e 98 case 'Organization':
bf86535e 99 case 'Household':
0103eaf0 100 case 'Formatting':
41fd8e26 101 return 'contact_1';
6a488035
TO
102 case 'Activity':
103 return 'activity_1';
104 case 'Contribution':
105 return 'contribution_1';
106 case 'Membership':
107 return 'membership_1';
108 case 'Participant':
109 return 'participant_1';
cf1182e6
N
110 case 'Case':
111 return 'case_1';
6a488035 112 default:
80eda9d1 113 if (CRM.contactSubTypes.length && ($.inArray(field_type,CRM.contactSubTypes) > -1)) {
21fced3b 114 return 'contact_1';
115 }
116 else {
117 throw "Cannot guess entity name for field_type=" + field_type;
118 }
6a488035 119 }
3e64165a 120 };
6a488035
TO
121
122 /**
123 * Represents a field in a customizable form.
124 */
125 CRM.UF.UFFieldModel = CRM.Backbone.Model.extend({
126 /**
e4f46be0 127 * Backbone.Form description of the field to which this refers
6a488035
TO
128 */
129 defaults: {
130 help_pre: '',
131 help_post: '',
132 /**
133 * @var bool, non-persistent indication of whether this field is unique or duplicate
134 * within its UFFieldCollection
135 */
136 is_duplicate: false
137 },
138 schema: {
139 'id': {
140 type: 'Number'
141 },
142 'uf_group_id': {
143 type: 'Number'
144 },
145 'entity_name': {
146 // pseudo-field
147 type: 'Text'
148 },
149 'field_name': {
150 type: 'Text'
151 },
152 'field_type': {
153 type: 'Select',
154 options: ['Contact', 'Individual', 'Organization', 'Contribution', 'Membership', 'Participant', 'Activity']
155 },
156 'help_post': {
157 title: addHelp(ts('Field Post Help'), {id: "help", file:"CRM/UF/Form/Field"}),
158 type: 'TextArea'
159 },
160 'help_pre': {
161 title: addHelp(ts('Field Pre Help'), {id: "help", file:"CRM/UF/Form/Field"}),
162 type: 'TextArea'
163 },
164 'in_selector': {
165 title: addHelp(ts('Results Columns?'), {id: "in_selector", file:"CRM/UF/Form/Field"}),
166 type: 'Select',
167 options: YESNO
168 },
169 'is_active': {
170 title: addHelp(ts('Active?'), {id: "is_active", file:"CRM/UF/Form/Field"}),
171 type: 'Select',
172 options: YESNO
173 },
174 'is_multi_summary': {
175 title: ts("Include in multi-record listing?"),
176 type: 'Select',
177 options: YESNO
178 },
179 'is_required': {
180 title: addHelp(ts('Required?'), {id: "is_required", file:"CRM/UF/Form/Field"}),
181 type: 'Select',
182 options: YESNO
183 },
184 'is_reserved': {
185 type: 'Select',
186 options: YESNO
187 },
188 'is_searchable': {
189 title: addHelp(ts("Searchable"), {id: "is_searchable", file:"CRM/UF/Form/Field"}),
190 type: 'Select',
191 options: YESNO
192 },
193 'is_view': {
194 title: addHelp(ts('View Only?'), {id: "is_view", file:"CRM/UF/Form/Field"}),
195 type: 'Select',
196 options: YESNO
197 },
198 'label': {
199 title: ts('Field Label'),
3a3663a4
CW
200 type: 'Text',
201 editorAttrs: {maxlength: 255}
6a488035
TO
202 },
203 'location_type_id': {
204 title: ts('Location Type'),
205 type: 'Select',
206 options: LOCATION_TYPES
207 },
1a228b4c 208 'website_type_id': {
209 title: ts('Website Type'),
210 type: 'Select',
211 options: WEBSITE_TYPES
212 },
6a488035
TO
213 'phone_type_id': {
214 title: ts('Phone Type'),
215 type: 'Select',
216 options: PHONE_TYPES
217 },
218 'visibility': {
219 title: addHelp(ts('Visibility'), {id: "visibility", file:"CRM/UF/Form/Field"}),
220 type: 'Select',
221 options: VISIBILITY
222 },
223 'weight': {
224 type: 'Number'
225 }
226 },
227 initialize: function() {
0103eaf0
CW
228 if (this.get('field_name').indexOf('formatting') === 0) {
229 this.schema.help_pre.title = ts('Markup');
230 }
6a488035
TO
231 this.set('entity_name', CRM.UF.guessEntityName(this.get('field_type')));
232 this.on("rel:ufGroupModel", this.applyDefaults, this);
233 this.on('change', watchChanges);
234 },
235 applyDefaults: function() {
236 var fieldSchema = this.getFieldSchema();
237 if (fieldSchema && fieldSchema.civiIsLocation && !this.get('location_type_id')) {
238 this.set('location_type_id', DEFAULT_LOCATION_TYPE_ID);
239 }
1a228b4c 240 if (fieldSchema && fieldSchema.civiIsWebsite && !this.get('website_type_id')) {
241 this.set('website_type_id', DEFAULT_WEBSITE_TYPE_ID);
242 }
6a488035
TO
243 if (fieldSchema && fieldSchema.civiIsPhone && !this.get('phone_type_id')) {
244 this.set('phone_type_id', DEFAULT_PHONE_TYPE_ID);
245 }
246 },
247 isInSelectorAllowed: function() {
248 var visibility = _.first(_.where(VISIBILITY, {val: this.get('visibility')}));
c1358f4e 249 if (visibility) {
250 return visibility.isInSelectorAllowed;
251 }
252 else {
253 return false;
254 }
6a488035
TO
255 },
256 getFieldSchema: function() {
257 return this.getRel('ufGroupModel').getFieldSchema(this.get('entity_name'), this.get('field_name'));
258 },
259 /**
260 * Create a uniqueness signature. Ideally, each UFField in a UFGroup should
261 * have a unique signature.
262 *
263 * @return {String}
264 */
265 getSignature: function() {
3e64165a
TO
266 return this.get("entity_name") +
267 '::' + this.get("field_name") +
1a228b4c 268 '::' + (this.get("location_type_id") ? this.get("location_type_id") : this.get("website_type_id") ? this.get("website_type_id") : '') +
3e64165a 269 '::' + (this.get("phone_type_id") ? this.get("phone_type_id") : '');
6a488035
TO
270 },
271
272 /**
273 * This is like destroy(), but it only destroys the item on the client-side;
274 * it does not trigger REST or Backbone.sync() operations.
275 *
276 * @return {Boolean}
277 */
278 destroyLocal: function() {
279 this.trigger('destroy', this, this.collection, {});
280 return false;
281 }
282 });
283
284 /**
285 * Represents a list of fields in a customizable form
286 *
287 * options:
288 * - uf_group_id: int
289 */
290 CRM.UF.UFFieldCollection = CRM.Backbone.Collection.extend({
291 model: CRM.UF.UFFieldModel,
292 uf_group_id: null, // int
293 initialize: function(models, options) {
294 options = options || {};
295 this.uf_group_id = options.uf_group_id;
296 this.initializeCopyToChildrenRelation('ufGroupModel', options.ufGroupModel, models);
297 this.on('add', this.watchDuplicates, this);
298 this.on('remove', this.unwatchDuplicates, this);
299 this.on('change', watchChanges);
300 this.on('add', watchChanges);
301 this.on('remove', watchChanges);
302 },
303 getFieldsByName: function(entityName, fieldName) {
304 return this.filter(function(ufFieldModel) {
305 return (ufFieldModel.get('entity_name') == entityName && ufFieldModel.get('field_name') == fieldName);
306 });
307 },
308 toSortedJSON: function() {
309 var fields = this.map(function(ufFieldModel){
310 return ufFieldModel.toStrictJSON();
311 });
312 return _.sortBy(fields, function(ufFieldJSON){
313 return parseInt(ufFieldJSON.weight);
314 });
315 },
316 isAddable: function(ufFieldModel) {
317 var entity_name = ufFieldModel.get('entity_name'),
318 field_name = ufFieldModel.get('field_name'),
319 fieldSchema = this.getRel('ufGroupModel').getFieldSchema(ufFieldModel.get('entity_name'), ufFieldModel.get('field_name'));
0103eaf0
CW
320 if (field_name.indexOf('formatting') === 0) {
321 return true;
322 }
6a488035
TO
323 if (! fieldSchema) {
324 return false;
325 }
326 var fields = this.getFieldsByName(entity_name, field_name);
327 var limit = 1;
328 if (fieldSchema.civiIsLocation) {
329 limit *= LOCATION_TYPES.length;
330 }
1a228b4c 331 if (fieldSchema.civiIsWebsite) {
332 limit *= WEBSITE_TYPES.length;
333 }
6a488035
TO
334 if (fieldSchema.civiIsPhone) {
335 limit *= PHONE_TYPES.length;
336 }
337 return fields.length < limit;
338 },
339 watchDuplicates: function(model, collection, options) {
340 model.on('change:location_type_id', this.markDuplicates, this);
1a228b4c 341 model.on('change:website_type_id', this.markDuplicates, this);
6a488035
TO
342 model.on('change:phone_type_id', this.markDuplicates, this);
343 this.markDuplicates();
344 },
345 unwatchDuplicates: function(model, collection, options) {
346 model.off('change:location_type_id', this.markDuplicates, this);
1a228b4c 347 model.off('change:website_type_id', this.markDuplicates, this);
6a488035
TO
348 model.off('change:phone_type_id', this.markDuplicates, this);
349 this.markDuplicates();
350 },
351 hasDuplicates: function() {
352 var firstDupe = this.find(function(ufFieldModel){
353 return ufFieldModel.get('is_duplicate');
354 });
355 return firstDupe ? true : false;
356 },
357 /**
358 *
359 */
360 markDuplicates: function() {
361 var ufFieldModelsByKey = this.groupBy(function(ufFieldModel) {
362 return ufFieldModel.getSignature();
363 });
364 this.each(function(ufFieldModel){
365 var is_duplicate = ufFieldModelsByKey[ufFieldModel.getSignature()].length > 1;
366 if (is_duplicate != ufFieldModel.get('is_duplicate')) {
367 ufFieldModel.set('is_duplicate', is_duplicate);
368 }
369 });
370 }
371 });
372
373 /**
374 * Represents an entity in a customizable form
375 */
376 CRM.UF.UFEntityModel = CRM.Backbone.Model.extend({
377 schema: {
378 'id': {
379 // title: ts(''),
380 type: 'Number'
381 },
382 'entity_name': {
383 title: ts('Entity Name'),
384 help: ts('Symbolic name which referenced in the fields'),
385 type: 'Text'
386 },
387 'entity_type': {
388 title: ts('Entity Type'),
389 type: 'Select',
390 options: ['IndividualModel', 'ActivityModel']
391 },
392 'entity_sub_type': {
393 // Use '*' to match all subtypes; use an int to match a specific type id; use empty-string to match none
394 title: ts('Sub Type'),
395 type: 'Text'
396 }
397 },
398 defaults: {
399 entity_sub_type: '*'
400 },
401 initialize: function() {
402 },
403 /**
404 * Get a list of all fields that can be used with this entity.
405 *
406 * @return {Object} keys are field names; values are fieldSchemas
407 */
408 getFieldSchemas: function() {
409 var ufEntityModel = this;
410 var modelClass= this.getModelClass();
411
412 if (this.get('entity_sub_type') == '*') {
413 return _.clone(modelClass.prototype.schema);
414 }
415
416 var result = {};
417 _.each(modelClass.prototype.schema, function(fieldSchema, fieldName){
418 var section = modelClass.prototype.sections[fieldSchema.section];
419 if (ufEntityModel.isSectionEnabled(section)) {
420 result[fieldName] = fieldSchema;
421 }
422 });
423 return result;
424 },
425 isSectionEnabled: function(section) {
99e239bc 426 //CRM-15427
427 return (!section || !section.extends_entity_column_value || _.contains(section.extends_entity_column_value, this.get('entity_sub_type')) || this.get('entity_sub_type') == '*');
6a488035
TO
428 },
429 getSections: function() {
430 var ufEntityModel = this;
431 var result = {};
432 _.each(ufEntityModel.getModelClass().prototype.sections, function(section, sectionKey){
433 if (ufEntityModel.isSectionEnabled(section)) {
434 result[sectionKey] = section;
435 }
436 });
437 return result;
438 },
439 getModelClass: function() {
440 return CRM.Schema[this.get('entity_type')];
441 }
442});
443
444 /**
445 * Represents a list of entities in a customizable form
446 *
447 * options:
448 * - ufGroupModel: UFGroupModel
449 */
450 CRM.UF.UFEntityCollection = CRM.Backbone.Collection.extend({
451 model: CRM.UF.UFEntityModel,
452 byName: {},
453 initialize: function(models, options) {
454 options = options || {};
455 this.initializeCopyToChildrenRelation('ufGroupModel', options.ufGroupModel, models);
456 },
457 /**
458 *
459 * @param name
460 * @return {UFEntityModel} if found; otherwise, null
461 */
462 getByName: function(name) {
463 // TODO consider indexing
464 return this.find(function(ufEntityModel){
465 return ufEntityModel.get('entity_name') == name;
466 });
467 }
468 });
469
470 /**
471 * Represents a customizable form
472 */
473 CRM.UF.UFGroupModel = CRM.Backbone.Model.extend({
474 defaults: {
d13aab87 475 title: ts('Unnamed Profile'),
6a488035
TO
476 is_active: 1
477 },
478 schema: {
479 'id': {
480 // title: ts(''),
481 type: 'Number'
482 },
483 'name': {
484 // title: ts(''),
485 type: 'Text'
486 },
487 'title': {
488 title: ts('Profile Name'),
489 help: ts(''),
490 type: 'Text',
3a3663a4 491 editorAttrs: {maxlength: 64},
6a488035
TO
492 validators: ['required']
493 },
ce1a9db4
SL
494 'frontend_title': {
495 title: ts('Public Title'),
496 help: ts(''),
497 type: 'Text',
498 editorAttrs: {maxlength: 64},
499 validators: []
500 },
6a488035
TO
501 'group_type': {
502 // For a description of group_type, see CRM_Core_BAO_UFGroup::updateGroupTypes
503 // title: ts(''),
504 type: 'Text'
505 },
506 'add_captcha': {
507 title: ts('Include reCAPTCHA?'),
508 help: ts('FIXME'),
509 type: 'Select',
510 options: YESNO
511 },
512 'add_to_group_id': {
513 title: ts('Add new contacts to a Group?'),
514 help: ts('Select a group if you are using this profile for adding new contacts, AND you want the new contacts to be automatically assigned to a group.'),
515 type: 'Number'
516 },
517 'cancel_URL': {
518 title: ts('Cancel Redirect URL'),
519 help: ts('If you are using this profile as a contact signup or edit form, and want to redirect the user to a static URL if they click the Cancel button - enter the complete URL here. If this field is left blank, the built-in Profile form will be redisplayed.'),
520 type: 'Text'
521 },
4aad2108
SL
522 'cancel_button_text': {
523 title: ts('Cancel Button Text'),
524 help: ts('Text to display on the cancel button when used in create or edit mode'),
525 type: 'Text'
526 },
527 'submit_button_text': {
528 title: ts('Submit Button Text'),
529 help: ts('Text to display on the submit button when used in create or edit mode'),
530 type: 'Text'
531 },
6a488035
TO
532 'created_date': {
533 //title: ts(''),
534 type: 'Text'// FIXME
535 },
536 'created_id': {
537 //title: ts(''),
538 type: 'Number'
539 },
540 'help_post': {
541 title: ts('Post-form Help'),
3e64165a
TO
542 help: ts('Explanatory text displayed at the end of the form.') +
543 ts('Note that this help text is displayed on profile create/edit screens only.'),
6a488035
TO
544 type: 'TextArea'
545 },
546 'help_pre': {
47775bc9 547 title: ts('Pre-form Help'),
3e64165a
TO
548 help: ts('Explanatory text displayed at the beginning of the form.') +
549 ts('Note that this help text is displayed on profile create/edit screens only.'),
6a488035
TO
550 type: 'TextArea'
551 },
552 'is_active': {
553 title: ts('Is this CiviCRM Profile active?'),
554 type: 'Select',
555 options: YESNO
556 },
557 'is_cms_user': {
558 title: ts('Drupal user account registration option?'),// FIXME
559 help: ts('FIXME'),
560 type: 'Select',
561 options: YESNO // FIXME
562 },
563 'is_edit_link': {
564 title: ts('Include profile edit links in search results?'),
565 help: ts('Check this box if you want to include a link in the listings to Edit profile fields. Only users with permission to edit the contact will see this link.'),
566 type: 'Select',
567 options: YESNO
568 },
569 'is_map': {
570 title: ts('Enable mapping for this profile?'),
571 help: ts('If enabled, a Map link is included on the profile listings rows and detail screens for any contacts whose records include sufficient location data for your mapping provider.'),
572 type: 'Select',
573 options: YESNO
574 },
575 'is_proximity_search': {
8e3d52a4 576 title: ts('Proximity Search'),
6a488035
TO
577 help: ts('FIXME'),
578 type: 'Select',
579 options: YESNO // FIXME
580 },
581 'is_reserved': {
582 // title: ts(''),
583 type: 'Select',
584 options: YESNO
585 },
586 'is_uf_link': {
587 title: ts('Include Drupal user account information links in search results?'), // FIXME
588 help: ts('FIXME'),
589 type: 'Select',
590 options: YESNO
591 },
592 'is_update_dupe': {
593 title: ts('What to do upon duplicate match'),
594 help: ts('FIXME'),
595 type: 'Select',
596 options: YESNO // FIXME
597 },
598 'limit_listings_group_id': {
599 title: ts('Limit listings to a specific Group?'),
600 help: ts('Select a group if you are using this profile for search and listings, AND you want to limit the listings to members of a specific group.'),
601 type: 'Number'
602 },
603 'notify': {
604 title: ts('Notify when profile form is submitted?'),
605 help: ts('If you want member(s) of your organization to receive a notification email whenever this Profile form is used to enter or update contact information, enter one or more email addresses here. Multiple email addresses should be separated by a comma (e.g. jane@example.org, paula@example.org). The first email address listed will be used as the FROM address in the notifications.'),
606 type: 'TextArea'
607 },
608 'post_URL': {
609 title: ts('Redirect URL'),
610 help: ts("If you are using this profile as a contact signup or edit form, and want to redirect the user to a static URL after they've submitted the form, you can also use contact tokens in URL - enter the complete URL here. If this field is left blank, the built-in Profile form will be redisplayed with a generic status message - 'Your contact information has been saved.'"),
611 type: 'Text'
612 },
613 'weight': {
614 title: ts('Order'),
615 help: ts('Weight controls the order in which profiles are presented when more than one profile is included in User Registration or My Account screens. Enter a positive or negative integer - lower numbers are displayed ahead of higher numbers.'),
616 type: 'Number'
617 // FIXME positive int
618 }
619 },
620 initialize: function() {
621 var ufGroupModel = this;
622
623 if (!this.getRel('ufEntityCollection')) {
624 var ufEntityCollection = new CRM.UF.UFEntityCollection([], {
625 ufGroupModel: this,
626 silent: false
627 });
628 this.setRel('ufEntityCollection', ufEntityCollection);
629 }
630
631 if (!this.getRel('ufFieldCollection')) {
632 var ufFieldCollection = new CRM.UF.UFFieldCollection([], {
633 uf_group_id: this.id,
634 ufGroupModel: this
635 });
636 this.setRel('ufFieldCollection', ufFieldCollection);
637 }
638
639 if (!this.getRel('paletteFieldCollection')) {
640 var paletteFieldCollection = new CRM.Designer.PaletteFieldCollection([], {
641 ufGroupModel: this
642 });
643 paletteFieldCollection.sync = function(method, model, options) {
3e64165a 644 if (!options) options = {};
6a488035
TO
645 // console.log(method, model, options);
646 switch (method) {
647 case 'read':
648 var success = options.success;
649 options.success = function(resp, status, xhr) {
650 if (success) success(resp, status, xhr);
651 model.trigger('sync', model, resp, options);
652 };
653 success(ufGroupModel.buildPaletteFields());
654
655 break;
656 case 'create':
657 case 'update':
658 case 'delete':
fb079bbf
TO
659 throw 'Unsupported method: ' + method;
660
6a488035
TO
661 default:
662 throw 'Unsupported method: ' + method;
663 }
664 };
665 this.setRel('paletteFieldCollection', paletteFieldCollection);
666 }
667
668 this.getRel('ufEntityCollection').on('reset', this.resetEntities, this);
669 this.resetEntities();
670
671 this.on('change', watchChanges);
672 },
673 /**
674 * Generate a copy of this UFGroupModel and its fields, with all ID's removed. The result
675 * is suitable for a new, identical UFGroup.
676 *
677 * @return {CRM.UF.UFGroupModel}
678 */
679 deepCopy: function() {
680 var copy = new CRM.UF.UFGroupModel(_.omit(this.toStrictJSON(), ['id','created_id','created_date','is_reserved','group_type']));
681 copy.getRel('ufEntityCollection').reset(
682 this.getRel('ufEntityCollection').toJSON()
683 // FIXME: for configurable entities, omit ['id', 'uf_group_id']
684 );
685 copy.getRel('ufFieldCollection').reset(
686 this.getRel('ufFieldCollection').map(function(ufFieldModel) {
687 return _.omit(ufFieldModel.toStrictJSON(), ['id', 'uf_group_id']);
688 })
689 );
e2beed76
SL
690 var new_id = 1;
691 CRM.api3('UFGroup', 'getsingle', {
692 "return": ["id"],
693 "options": {"limit": 1, "sort": "id DESC"}
694 }).done(function(result) {
695 new_id = Number(result.id) + 1;
696 var copyLabel = ' ' + ts('(Copy)');
697 var nameSuffix = '_' + new_id;
698 copy.set('title', copy.get('title').slice(0, 64 - copyLabel.length) + copyLabel);
699 copy.set('name', copy.get('name').slice(0, 64 - nameSuffix.length) + nameSuffix);
700 });
6a488035
TO
701 return copy;
702 },
703 getModelClass: function(entity_name) {
704 var ufEntity = this.getRel('ufEntityCollection').getByName(entity_name);
705 if (!ufEntity) throw 'Failed to locate entity: ' + entity_name;
706 return ufEntity.getModelClass();
707 },
708 getFieldSchema: function(entity_name, field_name) {
0103eaf0
CW
709 if (field_name.indexOf('formatting') === 0) {
710 field_name = 'formatting';
711 }
6a488035
TO
712 var modelClass = this.getModelClass(entity_name);
713 var fieldSchema = modelClass.prototype.schema[field_name];
714 if (!fieldSchema) {
0103eaf0 715 CRM.console('warn', 'Failed to locate field: ' + entity_name + "." + field_name);
6a488035
TO
716 return null;
717 }
718 return fieldSchema;
719 },
720 /**
721 * Check that the group_type contains *only* the types listed in validTypes
722 *
723 * @param string validTypesExpr
99e239bc 724 * @param bool allowAllSubtypes
6a488035
TO
725 * @return {Boolean}
726 */
99e239bc 727 //CRM-15427
37375016 728 checkGroupType: function(validTypesExpr, allowAllSubtypes, usedByFilter) {
6a488035 729 var allMatched = true;
f80ef0e2 730 allowAllSubtypes = allowAllSubtypes || false;
37375016 731 usedByFilter = usedByFilter || null;
fb079bbf 732 if (_.isEmpty(this.get('group_type'))) {
6a488035
TO
733 return true;
734 }
37375016 735 if (usedByFilter && _.isEmpty(this.get('module'))) {
736 return false;
737 }
6a488035
TO
738
739 var actualTypes = CRM.UF.parseTypeList(this.get('group_type'));
740 var validTypes = CRM.UF.parseTypeList(validTypesExpr);
741
742 // Every actual.coreType is a valid.coreType
743 _.each(actualTypes.coreTypes, function(ignore, actualCoreType) {
744 if (! validTypes.coreTypes[actualCoreType]) {
745 allMatched = false;
746 }
747 });
748
58770077 749 // CRM-16915 - filter with usedBy module if specified.
37375016 750 if (usedByFilter && this.get('module') != usedByFilter) {
751 allMatched = false;
752 }
99e239bc 753 //CRM-15427 allow all subtypes
754 if (!$.isEmptyObject(validTypes.subTypes) && !allowAllSubtypes) {
755 // Every actual.subType is a valid.subType
756 _.each(actualTypes.subTypes, function(actualSubTypeIds, actualSubTypeKey) {
757 if (!validTypes.subTypes[actualSubTypeKey]) {
758 allMatched = false;
759 return;
6a488035 760 }
99e239bc 761 // actualSubTypeIds is a list of all subtypes which can be used by group,
762 // so it's sufficient to match any one of them
763 var subTypeMatched = false;
764 _.each(actualSubTypeIds, function(ignore, actualSubTypeId) {
765 if (validTypes.subTypes[actualSubTypeKey][actualSubTypeId]) {
766 subTypeMatched = true;
767 }
768 });
769 allMatched = allMatched && subTypeMatched;
6a488035 770 });
99e239bc 771 }
6a488035
TO
772 return allMatched;
773 },
d35c2913 774 calculateContactEntityType: function() {
06575e57 775 var ufGroupModel = this;
776
777 // set proper entity model based on selected profile
778 var contactTypes = ['Individual', 'Household', 'Organization'];
45609fb1 779 var profileType = ufGroupModel.get('group_type') || '';
752ddf35
N
780
781 // check if selected profile have subtype defined eg: ["Individual,Contact,Case", "caseType:7"]
782 if (_.isArray(profileType) && profileType[0]) {
783 profileType = profileType[0];
784 }
06575e57 785 profileType = profileType.split(',');
cf1182e6 786
06575e57 787 var ufEntityModel;
788 _.each(profileType, function (ptype) {
789 if ($.inArray(ptype, contactTypes) > -1) {
f34dad55 790 ufEntityModel = ptype + 'Model';
06575e57 791 return true;
792 }
793 });
794
795 return ufEntityModel;
796 },
f34dad55 797 setUFGroupModel: function(entityType, allEntityModels) {
798 var ufGroupModel = this;
799
800 var newUfEntityModels = [];
801 _.each(allEntityModels, function (values) {
752ddf35 802 if (entityType && values.entity_name == 'contact_1') {
f34dad55 803 values.entity_type = entityType;
804 }
805 newUfEntityModels.push(new CRM.UF.UFEntityModel(values));
806 });
807
808 ufGroupModel.getRel('ufEntityCollection').reset(newUfEntityModels);
809 },
6a488035
TO
810 resetEntities: function() {
811 var ufGroupModel = this;
41fd8e26 812 var deleteFieldList = [];
6a488035
TO
813 ufGroupModel.getRel('ufFieldCollection').each(function(ufFieldModel){
814 if (!ufFieldModel.getFieldSchema()) {
41fd8e26 815 CRM.alert(ts('This profile no longer includes field "%1"! All references to the field have been removed.', {
816 1: ufFieldModel.get('label')
6a488035 817 }), '', 'alert', {expires: false});
41fd8e26 818 deleteFieldList.push(ufFieldModel);
6a488035
TO
819 }
820 });
41fd8e26 821
822 _.each(deleteFieldList, function(ufFieldModel) {
823 ufFieldModel.destroyLocal();
824 });
825
6a488035 826 this.getRel('paletteFieldCollection').reset(this.buildPaletteFields());
41fd8e26 827
828 // reset to redraw the cancel after entity type is updated.
829 ufGroupModel.getRel('ufFieldCollection').reset(ufGroupModel.getRel('ufFieldCollection').toJSON());
6a488035
TO
830 },
831 /**
832 *
833 * @return {Array} of PaletteFieldModel
834 */
835 buildPaletteFields: function() {
836 // rebuild list of fields; reuse old instances of PaletteFieldModel and create new ones
837 // as appropriate
838 // Note: The system as a whole is ill-defined in cases where we have an existing
839 // UFField that references a model field that disappears.
840
841 var ufGroupModel = this;
842
843 var oldPaletteFieldModelsBySig = {};
844 this.getRel('paletteFieldCollection').each(function(paletteFieldModel){
845 oldPaletteFieldModelsBySig[paletteFieldModel.get("entityName") + '::' + paletteFieldModel.get("fieldName")] = paletteFieldModel;
846 });
847
848 var newPaletteFieldModels = [];
849 this.getRel('ufEntityCollection').each(function(ufEntityModel){
850 var modelClass = ufEntityModel.getModelClass();
851 _.each(ufEntityModel.getFieldSchemas(), function(value, key, list) {
852 var model = oldPaletteFieldModelsBySig[ufEntityModel.get('entity_name') + '::' + key];
853 if (!model) {
854 model = new CRM.Designer.PaletteFieldModel({
855 modelClass: modelClass,
856 entityName: ufEntityModel.get('entity_name'),
857 fieldName: key
858 });
859 }
860 newPaletteFieldModels.push(model);
861 });
862 });
863
864 return newPaletteFieldModels;
865 }
866 });
867
868 /**
869 * Represents a list of customizable form
870 */
871 CRM.UF.UFGroupCollection = CRM.Backbone.Collection.extend({
872 model: CRM.UF.UFGroupModel
873 });
4b513f23 874})(CRM.$, CRM._);