Merge pull request #16265 from seamuslee001/fix_flexmailer
[civicrm-core.git] / ang / api4Explorer / Explorer.js
1 (function(angular, $, _, undefined) {
2
3 // Schema metadata
4 var schema = CRM.vars.api4.schema;
5 // FK schema data
6 var links = CRM.vars.api4.links;
7 // Cache list of entities
8 var entities = [];
9 // Cache list of actions
10 var actions = [];
11 // Field options
12 var fieldOptions = {};
13
14
15 angular.module('api4Explorer').config(function($routeProvider) {
16 $routeProvider.when('/explorer/:api4entity?/:api4action?', {
17 controller: 'Api4Explorer',
18 templateUrl: '~/api4Explorer/Explorer.html',
19 reloadOnSearch: false
20 });
21 });
22
23 angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4) {
24 var ts = $scope.ts = CRM.ts();
25 $scope.entities = entities;
26 $scope.actions = actions;
27 $scope.fields = [];
28 $scope.fieldsAndJoins = [];
29 $scope.availableParams = {};
30 $scope.params = {};
31 $scope.index = '';
32 var getMetaParams = {},
33 objectParams = {orderBy: 'ASC', values: '', chain: ['Entity', '', '{}']},
34 docs = CRM.vars.api4.docs,
35 helpTitle = '',
36 helpContent = {};
37 $scope.helpTitle = '';
38 $scope.helpContent = {};
39 $scope.entity = $routeParams.api4entity;
40 $scope.result = [];
41 $scope.status = 'default';
42 $scope.loading = false;
43 $scope.controls = {};
44 $scope.codeLabel = {
45 oop: ts('PHP (oop style)'),
46 php: ts('PHP (traditional)'),
47 js: ts('Javascript'),
48 cli: ts('Command Line')
49 };
50 $scope.code = codeDefaults();
51
52 function codeDefaults() {
53 return _.mapValues($scope.codeLabel, function(val, key) {
54 return key === 'oop' ? ts('Select an entity and action') : '';
55 });
56 }
57
58 if (!entities.length) {
59 formatForSelect2(schema, entities, 'name', ['description']);
60 }
61
62 $scope.$bindToRoute({
63 expr: 'index',
64 param: 'index',
65 default: ''
66 });
67
68 function ucfirst(str) {
69 return str[0].toUpperCase() + str.slice(1);
70 }
71
72 function lcfirst(str) {
73 return str[0].toLowerCase() + str.slice(1);
74 }
75
76 function pluralize(str) {
77 switch (str[str.length-1]) {
78 case 's':
79 return str + 'es';
80 case 'y':
81 return str.slice(0, -1) + 'ies';
82 default:
83 return str + 's';
84 }
85 }
86
87 // Turn a flat array into a select2 array
88 function arrayToSelect2(array) {
89 var out = [];
90 _.each(array, function(item) {
91 out.push({id: item, text: item});
92 });
93 return out;
94 }
95
96 // Reformat an existing array of objects for compatibility with select2
97 function formatForSelect2(input, container, key, extra, prefix) {
98 _.each(input, function(item) {
99 var id = (prefix || '') + item[key];
100 var formatted = {id: id, text: id};
101 if (extra) {
102 _.merge(formatted, _.pick(item, extra));
103 }
104 container.push(formatted);
105 });
106 return container;
107 }
108
109 function getFieldList(action) {
110 var fields = [],
111 fieldInfo = _.findWhere(getEntity().actions, {name: action}).fields;
112 formatForSelect2(fieldInfo, fields, 'name', ['description', 'required', 'default_value']);
113 return fields;
114 }
115
116 function addJoins(fieldList) {
117 var fields = _.cloneDeep(fieldList),
118 fks = _.findWhere(links, {entity: $scope.entity}) || {};
119 _.each(fks.links, function(link) {
120 var linkFields = entityFields(link.entity);
121 if (linkFields) {
122 fields.push({
123 text: link.alias,
124 description: 'Join to ' + link.entity,
125 children: formatForSelect2(linkFields, [], 'name', ['description'], link.alias + '.')
126 });
127 }
128 });
129 return fields;
130 }
131
132 $scope.help = function(title, param) {
133 if (!param) {
134 $scope.helpTitle = helpTitle;
135 $scope.helpContent = helpContent;
136 } else {
137 $scope.helpTitle = title;
138 $scope.helpContent = param;
139 }
140 };
141
142 $scope.fieldHelp = function(fieldName) {
143 var field = getField(fieldName, $scope.entity, $scope.action);
144 if (!field) {
145 return;
146 }
147 var info = {
148 description: field.description,
149 type: field.data_type
150 };
151 if (field.default_value) {
152 info.default = field.default_value;
153 }
154 if (field.required_if) {
155 info.required_if = field.required_if;
156 } else if (field.required) {
157 info.required = 'true';
158 }
159 return info;
160 };
161
162 $scope.valuesFields = function() {
163 var fields = _.cloneDeep($scope.action === 'getFields' ? getFieldList($scope.params.action || 'get') : $scope.fields);
164 // Disable fields that are already in use
165 _.each($scope.params.values || [], function(val) {
166 (_.findWhere(fields, {id: val[0]}) || {}).disabled = true;
167 });
168 return {results: fields};
169 };
170
171 $scope.formatSelect2Item = function(row) {
172 return _.escape(row.text) +
173 (row.required ? '<span class="crm-marker"> *</span>' : '') +
174 (row.description ? '<div class="crm-select2-row-description"><p>' + _.escape(row.description) + '</p></div>' : '');
175 };
176
177 $scope.clearParam = function(name) {
178 $scope.params[name] = $scope.availableParams[name].default;
179 };
180
181 $scope.isSpecial = function(name) {
182 var specialParams = ['select', 'fields', 'action', 'where', 'values', 'orderBy', 'chain'];
183 return _.contains(specialParams, name);
184 };
185
186 $scope.selectRowCount = function() {
187 if ($scope.isSelectRowCount()) {
188 $scope.params.select = [];
189 } else {
190 $scope.params.select = ['row_count'];
191 $scope.index = '';
192 if ($scope.params.limit == 25) {
193 $scope.params.limit = 0;
194 }
195 }
196 };
197
198 $scope.isSelectRowCount = function() {
199 return $scope.params && $scope.params.select && $scope.params.select.length === 1 && $scope.params.select[0] === 'row_count';
200 };
201
202 function getEntity(entityName) {
203 return _.findWhere(schema, {name: entityName || $scope.entity});
204 }
205
206 // Get all params that have been set
207 function getParams() {
208 var params = {};
209 _.each($scope.params, function(param, key) {
210 if (param != $scope.availableParams[key].default && !(typeof param === 'object' && _.isEmpty(param))) {
211 if (_.contains($scope.availableParams[key].type, 'array') && (typeof objectParams[key] === 'undefined')) {
212 params[key] = parseYaml(JSON.parse(angular.toJson(param)));
213 } else {
214 params[key] = param;
215 }
216 }
217 });
218 _.each(objectParams, function(defaultVal, key) {
219 if (params[key]) {
220 var newParam = {};
221 _.each(params[key], function(item) {
222 var val = _.cloneDeep(item[1]);
223 // Remove blank items from "chain" array
224 if (_.isArray(val)) {
225 _.eachRight(item[1], function(v, k) {
226 if (v) {
227 return false;
228 }
229 val.length--;
230 });
231 }
232 newParam[item[0]] = parseYaml(val);
233 });
234 params[key] = newParam;
235 }
236 });
237 return params;
238 }
239
240 function parseYaml(input) {
241 if (typeof input === 'undefined') {
242 return undefined;
243 }
244 if (input === '') {
245 return '';
246 }
247 if (_.isObject(input) || _.isArray(input)) {
248 _.each(input, function(item, index) {
249 input[index] = parseYaml(item);
250 });
251 return input;
252 }
253 try {
254 var output = (input === '>') ? '>' : jsyaml.safeLoad(input);
255 // We don't want dates parsed to js objects
256 return _.isDate(output) ? input : output;
257 } catch (e) {
258 return input;
259 }
260 }
261
262 function selectAction() {
263 $scope.action = $routeParams.api4action;
264 $scope.fieldsAndJoins = [];
265 if (!actions.length) {
266 formatForSelect2(getEntity().actions, actions, 'name', ['description', 'params']);
267 }
268 if ($scope.action) {
269 var actionInfo = _.findWhere(actions, {id: $scope.action});
270 $scope.fields = getFieldList($scope.action);
271 if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
272 $scope.fieldsAndJoins = addJoins($scope.fields);
273 } else {
274 $scope.fieldsAndJoins = $scope.fields;
275 }
276 _.each(actionInfo.params, function (param, name) {
277 var format,
278 defaultVal = _.cloneDeep(param.default);
279 if (param.type) {
280 switch (param.type[0]) {
281 case 'int':
282 case 'bool':
283 format = param.type[0];
284 break;
285
286 case 'array':
287 case 'object':
288 format = 'json';
289 break;
290
291 default:
292 format = 'raw';
293 }
294 if (name == 'limit') {
295 defaultVal = 25;
296 }
297 if (name === 'values') {
298 defaultVal = defaultValues(defaultVal);
299 }
300 $scope.$bindToRoute({
301 expr: 'params["' + name + '"]',
302 param: name,
303 format: format,
304 default: defaultVal,
305 deep: format === 'json'
306 });
307 }
308 if (typeof objectParams[name] !== 'undefined') {
309 $scope.$watch('params.' + name, function(values) {
310 // Remove empty values
311 _.each(values, function(clause, index) {
312 if (!clause || !clause[0]) {
313 $scope.params[name].splice(index, 1);
314 }
315 });
316 }, true);
317 $scope.$watch('controls.' + name, function(value) {
318 var field = value;
319 $timeout(function() {
320 if (field) {
321 var defaultOp = _.cloneDeep(objectParams[name]);
322 if (name === 'chain') {
323 var num = $scope.params.chain.length;
324 defaultOp[0] = field;
325 field = 'name_me_' + num;
326 }
327 $scope.params[name].push([field, defaultOp]);
328 $scope.controls[name] = null;
329 }
330 });
331 });
332 }
333 });
334 $scope.availableParams = actionInfo.params;
335 }
336 writeCode();
337 }
338
339 function defaultValues(defaultVal) {
340 _.each($scope.fields, function(field) {
341 if (field.required) {
342 defaultVal.push([field.id, '']);
343 }
344 });
345 return defaultVal;
346 }
347
348 function stringify(value, trim) {
349 if (typeof value === 'undefined') {
350 return '';
351 }
352 var str = JSON.stringify(value).replace(/,/g, ', ');
353 if (trim) {
354 str = str.slice(1, -1);
355 }
356 return str.trim();
357 }
358
359 function writeCode() {
360 var code = codeDefaults(),
361 entity = $scope.entity,
362 action = $scope.action,
363 params = getParams(),
364 index = isInt($scope.index) ? +$scope.index : parseYaml($scope.index),
365 result = 'result';
366 if ($scope.entity && $scope.action) {
367 if (action.slice(0, 3) === 'get') {
368 result = entity.substr(0, 7) === 'Custom_' ? _.camelCase(entity.substr(7)) : entity;
369 result = lcfirst(action.replace(/s$/, '').slice(3) || result);
370 }
371 var results = lcfirst(_.isNumber(index) ? result : pluralize(result)),
372 paramCount = _.size(params),
373 isSelectRowCount = params.select && params.select.length === 1 && params.select[0] === 'row_count',
374 i = 0;
375
376 if (isSelectRowCount) {
377 results = result + 'Count';
378 }
379
380 // Write javascript
381 code.js = "CRM.api4('" + entity + "', '" + action + "', {";
382 _.each(params, function(param, key) {
383 code.js += "\n " + key + ': ' + stringify(param) +
384 (++i < paramCount ? ',' : '');
385 if (key === 'checkPermissions') {
386 code.js += ' // IGNORED: permissions are always enforced from client-side requests';
387 }
388 });
389 code.js += "\n}";
390 if (index || index === 0) {
391 code.js += ', ' + JSON.stringify(index);
392 }
393 code.js += ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
394
395 // Write php code
396 code.php = '$' + results + " = civicrm_api4('" + entity + "', '" + action + "', [";
397 _.each(params, function(param, key) {
398 code.php += "\n '" + key + "' => " + phpFormat(param, 4) + ',';
399 });
400 code.php += "\n]";
401 if (index || index === 0) {
402 code.php += ', ' + phpFormat(index);
403 }
404 code.php += ");";
405
406 // Write oop code
407 if (entity.substr(0, 7) !== 'Custom_') {
408 code.oop = '$' + results + " = \\Civi\\Api4\\" + entity + '::' + action + '()';
409 } else {
410 code.oop = '$' + results + " = \\Civi\\Api4\\CustomValue::" + action + "('" + entity.substr(7) + "')";
411 }
412 _.each(params, function(param, key) {
413 var val = '';
414 if (typeof objectParams[key] !== 'undefined' && key !== 'chain') {
415 _.each(param, function(item, index) {
416 val = phpFormat(index) + ', ' + phpFormat(item, 4);
417 code.oop += "\n ->add" + ucfirst(key).replace(/s$/, '') + '(' + val + ')';
418 });
419 } else if (key === 'where') {
420 _.each(param, function (clause) {
421 if (clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT') {
422 code.oop += "\n ->addClause(" + phpFormat(clause[0]) + ", " + phpFormat(clause[1]).slice(1, -1) + ')';
423 } else {
424 code.oop += "\n ->addWhere(" + phpFormat(clause).slice(1, -1) + ")";
425 }
426 });
427 } else if (key === 'select' && isSelectRowCount) {
428 code.oop += "\n ->selectRowCount()";
429 } else {
430 code.oop += "\n ->set" + ucfirst(key) + '(' + phpFormat(param, 4) + ')';
431 }
432 });
433 code.oop += "\n ->execute()";
434 if (isSelectRowCount) {
435 code.oop += "\n ->count()";
436 } else if (_.isNumber(index)) {
437 code.oop += !index ? '\n ->first()' : (index === -1 ? '\n ->last()' : '\n ->itemAt(' + index + ')');
438 } else if (index) {
439 if (_.isString(index) || (_.isPlainObject(index) && !index[0] && !index['0'])) {
440 code.oop += "\n ->indexBy('" + (_.isPlainObject(index) ? _.keys(index)[0] : index) + "')";
441 }
442 if (_.isArray(index) || _.isPlainObject(index)) {
443 code.oop += "\n ->column('" + (_.isArray(index) ? index[0] : _.values(index)[0]) + "')";
444 }
445 }
446 code.oop += ";\n";
447 if (!_.isNumber(index) && !isSelectRowCount) {
448 code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n // do something\n}';
449 }
450
451 // Write cli code
452 code.cli = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
453 }
454 _.each(code, function(val, type) {
455 $scope.code[type] = prettyPrintOne(_.escape(val));
456 });
457 }
458
459 function isInt(value) {
460 if (_.isFinite(value)) {
461 return true;
462 }
463 if (!_.isString(value)) {
464 return false;
465 }
466 return /^-{0,1}\d+$/.test(value);
467 }
468
469 function formatMeta(resp) {
470 var ret = '';
471 _.each(resp, function(val, key) {
472 if (key !== 'values' && !_.isPlainObject(val) && !_.isFunction(val)) {
473 ret += (ret.length ? ', ' : '') + key + ': ' + (_.isArray(val) ? '[' + val + ']' : val);
474 }
475 });
476 return prettyPrintOne(_.escape(ret));
477 }
478
479 $scope.execute = function() {
480 $scope.status = 'warning';
481 $scope.loading = true;
482 $http.post(CRM.url('civicrm/ajax/api4/' + $scope.entity + '/' + $scope.action, {
483 params: angular.toJson(getParams()),
484 index: isInt($scope.index) ? +$scope.index : parseYaml($scope.index)
485 }), null, {
486 headers: {
487 'X-Requested-With': 'XMLHttpRequest'
488 }
489 }).then(function(resp) {
490 $scope.loading = false;
491 $scope.status = 'success';
492 $scope.result = [formatMeta(resp.data), prettyPrintOne(_.escape(JSON.stringify(resp.data.values, null, 2)), 'js', 1)];
493 }, function(resp) {
494 $scope.loading = false;
495 $scope.status = 'danger';
496 $scope.result = [formatMeta(resp), prettyPrintOne(_.escape(JSON.stringify(resp.data, null, 2)))];
497 });
498 };
499
500 /**
501 * Format value to look like php code
502 */
503 function phpFormat(val, indent) {
504 if (typeof val === 'undefined') {
505 return '';
506 }
507 if (val === null || val === true || val === false) {
508 return JSON.stringify(val).toUpperCase();
509 }
510 indent = (typeof indent === 'number') ? _.repeat(' ', indent) : (indent || '');
511 var ret = '',
512 baseLine = indent ? indent.slice(0, -2) : '',
513 newLine = indent ? '\n' : '',
514 trailingComma = indent ? ',' : '';
515 if ($.isPlainObject(val)) {
516 $.each(val, function(k, v) {
517 ret += (ret ? ', ' : '') + newLine + indent + "'" + k + "' => " + phpFormat(v);
518 });
519 return '[' + ret + trailingComma + newLine + baseLine + ']';
520 }
521 if ($.isArray(val)) {
522 $.each(val, function(k, v) {
523 ret += (ret ? ', ' : '') + newLine + indent + phpFormat(v);
524 });
525 return '[' + ret + trailingComma + newLine + baseLine + ']';
526 }
527 if (_.isString(val) && !_.contains(val, "'")) {
528 return "'" + val + "'";
529 }
530 return JSON.stringify(val).replace(/\$/g, '\\$');
531 }
532
533 function fetchMeta() {
534 crmApi4(getMetaParams)
535 .then(function(data) {
536 if (data.actions) {
537 getEntity().actions = data.actions;
538 selectAction();
539 }
540 });
541 }
542
543 // Help for an entity with no action selected
544 function showEntityHelp(entityName) {
545 var entityInfo = getEntity(entityName);
546 $scope.helpTitle = helpTitle = $scope.entity;
547 $scope.helpContent = helpContent = {
548 description: entityInfo.description,
549 comment: entityInfo.comment
550 };
551 }
552
553 if (!$scope.entity) {
554 $scope.helpTitle = helpTitle = ts('APIv4 Explorer');
555 $scope.helpContent = helpContent = {description: docs.description, comment: docs.comment};
556 } else if (!actions.length && !getEntity().actions) {
557 getMetaParams.actions = [$scope.entity, 'getActions', {chain: {fields: [$scope.entity, 'getFields', {action: '$name'}]}}];
558 fetchMeta();
559 } else {
560 selectAction();
561 }
562
563 if ($scope.entity) {
564 showEntityHelp($scope.entity);
565 }
566
567 // Update route when changing entity
568 $scope.$watch('entity', function(newVal, oldVal) {
569 if (oldVal !== newVal) {
570 // Flush actions cache to re-fetch for new entity
571 actions = [];
572 $location.url('/explorer/' + newVal);
573 }
574 });
575
576 // Update route when changing actions
577 $scope.$watch('action', function(newVal, oldVal) {
578 if ($scope.entity && $routeParams.api4action !== newVal && !_.isUndefined(newVal)) {
579 $location.url('/explorer/' + $scope.entity + '/' + newVal);
580 } else if (newVal) {
581 $scope.helpTitle = helpTitle = $scope.entity + '::' + newVal;
582 $scope.helpContent = helpContent = _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment']);
583 }
584 });
585
586 $scope.paramDoc = function(name) {
587 return docs.params[name];
588 };
589
590 $scope.$watch('params', writeCode, true);
591 $scope.$watch('index', writeCode);
592 writeCode();
593
594 });
595
596 angular.module('api4Explorer').directive('crmApi4WhereClause', function($timeout) {
597 return {
598 scope: {
599 data: '=crmApi4WhereClause'
600 },
601 templateUrl: '~/api4Explorer/WhereClause.html',
602 link: function (scope, element, attrs) {
603 var ts = scope.ts = CRM.ts();
604 scope.newClause = '';
605 scope.conjunctions = ['AND', 'OR', 'NOT'];
606 scope.operators = CRM.vars.api4.operators;
607
608 scope.addGroup = function(op) {
609 scope.data.where.push([op, []]);
610 };
611
612 scope.removeGroup = function() {
613 scope.data.groupParent.splice(scope.data.groupIndex, 1);
614 };
615
616 scope.onSort = function(event, ui) {
617 $('.api4-where-fieldset').toggleClass('api4-sorting', event.type === 'sortstart');
618 $('.api4-input.form-inline').css('margin-left', '');
619 };
620
621 // Indent clause while dragging between nested groups
622 scope.onSortOver = function(event, ui) {
623 var offset = 0;
624 if (ui.sender) {
625 offset = $(ui.placeholder).offset().left - $(ui.sender).offset().left;
626 }
627 $('.api4-input.form-inline.ui-sortable-helper').css('margin-left', '' + offset + 'px');
628 };
629
630 scope.$watch('newClause', function(value) {
631 var field = value;
632 $timeout(function() {
633 if (field) {
634 scope.data.where.push([field, '=', '']);
635 scope.newClause = null;
636 }
637 });
638 });
639 scope.$watch('data.where', function(values) {
640 // Remove empty values
641 _.each(values, function(clause, index) {
642 if (typeof clause !== 'undefined' && !clause[0]) {
643 values.splice(index, 1);
644 }
645 if (typeof clause[1] === 'string' && _.contains(clause[1], 'NULL')) {
646 clause.length = 2;
647 } else if (typeof clause[1] === 'string' && clause.length == 2) {
648 clause.push('');
649 }
650 });
651 }, true);
652 }
653 };
654 });
655
656 angular.module('api4Explorer').directive('api4ExpValue', function($routeParams, crmApi4) {
657 return {
658 scope: {
659 data: '=api4ExpValue'
660 },
661 require: 'ngModel',
662 link: function (scope, element, attrs, ctrl) {
663 var ts = scope.ts = CRM.ts(),
664 multi = _.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], scope.data.op),
665 entity = $routeParams.api4entity,
666 action = scope.data.action || $routeParams.api4action;
667
668 function destroyWidget() {
669 var $el = $(element);
670 if ($el.is('.crm-form-date-wrapper .crm-hidden-date')) {
671 $el.crmDatepicker('destroy');
672 }
673 if ($el.is('.select2-container + input')) {
674 $el.crmEntityRef('destroy');
675 }
676 $(element).removeData().removeAttr('type').removeAttr('placeholder').show();
677 }
678
679 function makeWidget(field, op) {
680 var $el = $(element),
681 inputType = field.input_type,
682 dataType = field.data_type;
683 if (!op) {
684 op = field.serialize || dataType === 'Array' ? 'IN' : '=';
685 }
686 multi = _.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], op);
687 if (op === 'IS NULL' || op === 'IS NOT NULL') {
688 $el.hide();
689 return;
690 }
691 if (inputType === 'Date') {
692 if (_.includes(['=', '!=', '<>', '<', '>=', '<', '<='], op)) {
693 $el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false});
694 }
695 } else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op) && (field.fk_entity || field.options || dataType === 'Boolean')) {
696 if (field.fk_entity) {
697 $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
698 } else if (field.options) {
699 $el.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple: multi, data: [{id: '', text: ''}]});
700 loadFieldOptions(field.entity || entity).then(function(data) {
701 var options = [];
702 _.each(_.findWhere(data, {name: field.name}).options, function(val, key) {
703 options.push({id: key, text: val});
704 });
705 $el.removeClass('loading').select2({data: options, multiple: multi});
706 });
707 } else if (dataType === 'Boolean') {
708 $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [
709 {id: '1', text: ts('Yes')},
710 {id: '0', text: ts('No')}
711 ]});
712 }
713 } else if (dataType === 'Integer' && !multi) {
714 $el.attr('type', 'number');
715 }
716 }
717
718 function loadFieldOptions(entity) {
719 if (!fieldOptions[entity + action]) {
720 fieldOptions[entity + action] = crmApi4(entity, 'getFields', {
721 loadOptions: true,
722 action: action,
723 where: [["options", "!=", false]],
724 select: ["name", "options"]
725 });
726 }
727 return fieldOptions[entity + action];
728 }
729
730 // Copied from ng-list but applied conditionally if field is multi-valued
731 var parseList = function(viewValue) {
732 // If the viewValue is invalid (say required but empty) it will be `undefined`
733 if (_.isUndefined(viewValue)) return;
734
735 if (!multi) {
736 return viewValue;
737 }
738
739 var list = [];
740
741 if (viewValue) {
742 _.each(viewValue.split(','), function(value) {
743 if (value) list.push(_.trim(value));
744 });
745 }
746
747 return list;
748 };
749
750 // Copied from ng-list
751 ctrl.$parsers.push(parseList);
752 ctrl.$formatters.push(function(value) {
753 return _.isArray(value) ? value.join(', ') : value;
754 });
755
756 // Copied from ng-list
757 ctrl.$isEmpty = function(value) {
758 return !value || !value.length;
759 };
760
761 scope.$watchCollection('data', function(data) {
762 destroyWidget();
763 var field = getField(data.field, entity, action);
764 if (field) {
765 makeWidget(field, data.op);
766 }
767 });
768 }
769 };
770 });
771
772
773 angular.module('api4Explorer').directive('api4ExpChain', function(crmApi4) {
774 return {
775 scope: {
776 chain: '=api4ExpChain',
777 mainEntity: '=',
778 entities: '='
779 },
780 templateUrl: '~/api4Explorer/Chain.html',
781 link: function (scope, element, attrs) {
782 var ts = scope.ts = CRM.ts();
783
784 function changeEntity(newEntity, oldEntity) {
785 // When clearing entity remove this chain
786 if (!newEntity) {
787 scope.chain[0] = '';
788 return;
789 }
790 // Reset action && index
791 if (newEntity !== oldEntity) {
792 scope.chain[1][1] = scope.chain[1][2] = '';
793 }
794 if (getEntity(newEntity).actions) {
795 setActions();
796 } else {
797 crmApi4(newEntity, 'getActions', {chain: {fields: [newEntity, 'getFields', {action: '$name'}]}})
798 .then(function(data) {
799 getEntity(data.entity).actions = data;
800 if (data.entity === scope.chain[1][0]) {
801 setActions();
802 }
803 });
804 }
805 }
806
807 function setActions() {
808 scope.actions = [''].concat(_.pluck(getEntity(scope.chain[1][0]).actions, 'name'));
809 }
810
811 // Set default params when choosing action
812 function changeAction(newAction, oldAction) {
813 var link;
814 // Prepopulate links
815 if (newAction && newAction !== oldAction) {
816 // Clear index
817 scope.chain[1][3] = '';
818 // Look for links back to main entity
819 _.each(entityFields(scope.chain[1][0]), function(field) {
820 if (field.fk_entity === scope.mainEntity) {
821 link = [field.name, '$id'];
822 }
823 });
824 // Look for links from main entity
825 if (!link && newAction !== 'create') {
826 _.each(entityFields(scope.mainEntity), function(field) {
827 if (field.fk_entity === scope.chain[1][0]) {
828 link = ['id', '$' + field.name];
829 // Since we're specifying the id, set index to getsingle
830 scope.chain[1][3] = '0';
831 }
832 });
833 }
834 if (link && _.contains(['get', 'update', 'replace', 'delete'], newAction)) {
835 scope.chain[1][2] = '{where: [[' + link[0] + ', =, ' + link[1] + ']]}';
836 }
837 else if (link && _.contains(['create'], newAction)) {
838 scope.chain[1][2] = '{values: {' + link[0] + ': ' + link[1] + '}}';
839 }
840 else if (link && _.contains(['save'], newAction)) {
841 scope.chain[1][2] = '{records: [{' + link[0] + ': ' + link[1] + '}]}';
842 } else {
843 scope.chain[1][2] = '{}';
844 }
845 }
846 }
847
848 scope.$watch("chain[1][0]", changeEntity);
849 scope.$watch("chain[1][1]", changeAction);
850 }
851 };
852 });
853
854 function getEntity(entityName) {
855 return _.findWhere(schema, {name: entityName});
856 }
857
858 function entityFields(entityName, action) {
859 var entity = getEntity(entityName);
860 if (entity && action && entity.actions) {
861 return _.findWhere(entity.actions, {name: action}).fields;
862 }
863 return _.result(entity, 'fields');
864 }
865
866 function getField(fieldName, entity, action) {
867 var fieldNames = fieldName.split('.');
868 return get(entity, fieldNames);
869
870 function get(entity, fieldNames) {
871 if (fieldNames.length === 1) {
872 return _.findWhere(entityFields(entity, action), {name: fieldNames[0]});
873 }
874 var comboName = _.findWhere(entityFields(entity, action), {name: fieldNames[0] + '.' + fieldNames[1]});
875 if (comboName) {
876 return comboName;
877 }
878 var linkName = fieldNames.shift(),
879 entityLinks = _.findWhere(links, {entity: entity}).links,
880 newEntity = _.findWhere(entityLinks, {alias: linkName}).entity;
881 return get(newEntity, fieldNames);
882 }
883 }
884
885 // Collapsible optgroups for select2
886 $(function() {
887 $('body')
888 .on('select2-open', function(e) {
889 if ($(e.target).hasClass('collapsible-optgroups')) {
890 $('#select2-drop')
891 .off('.collapseOptionGroup')
892 .addClass('collapsible-optgroups-enabled')
893 .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() {
894 $(this).parent().toggleClass('optgroup-expanded');
895 });
896 }
897 })
898 .on('select2-close', function() {
899 $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled');
900 });
901 });
902 })(angular, CRM.$, CRM._);