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