Merge pull request #17741 from eileenmcnaughton/eventcart
[civicrm-core.git] / ang / api4Explorer / Explorer.js
CommitLineData
19b53e5b
C
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 = {};
f54beb1e
CW
13 // Api params
14 var params;
19b53e5b
C
15
16
17 angular.module('api4Explorer').config(function($routeProvider) {
18 $routeProvider.when('/explorer/:api4entity?/:api4action?', {
19 controller: 'Api4Explorer',
20 templateUrl: '~/api4Explorer/Explorer.html',
21 reloadOnSearch: false
22 });
23 });
24
4e97c268 25 angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4, dialogService) {
f54beb1e
CW
26 var ts = $scope.ts = CRM.ts(),
27 ctrl = $scope.$ctrl = this;
19b53e5b
C
28 $scope.entities = entities;
29 $scope.actions = actions;
30 $scope.fields = [];
9f6c0e4f 31 $scope.havingOptions = [];
19b53e5b 32 $scope.fieldsAndJoins = [];
a26e006b 33 $scope.fieldsAndJoinsAndFunctions = [];
a4499ec5 34 $scope.fieldsAndJoinsAndFunctionsWithSuffixes = [];
a26e006b 35 $scope.fieldsAndJoinsAndFunctionsAndWildcards = [];
19b53e5b 36 $scope.availableParams = {};
f54beb1e 37 params = $scope.params = {};
19b53e5b 38 $scope.index = '';
19d01932 39 $scope.selectedTab = {result: 'result', code: 'php'};
b65fa6dc 40 $scope.perm = {
d7507e89
CW
41 accessDebugOutput: CRM.checkPerm('access debug output'),
42 editGroups: CRM.checkPerm('edit groups')
b65fa6dc 43 };
136ca5bb 44 marked.setOptions({highlight: prettyPrintOne});
19b53e5b 45 var getMetaParams = {},
bb3786d2 46 objectParams = {orderBy: 'ASC', values: '', defaults: '', chain: ['Entity', '', '{}']},
9cea3619 47 docs = CRM.vars.api4.docs,
19b53e5b
C
48 helpTitle = '',
49 helpContent = {};
50 $scope.helpTitle = '';
51 $scope.helpContent = {};
52 $scope.entity = $routeParams.api4entity;
53 $scope.result = [];
b65fa6dc 54 $scope.debug = null;
19b53e5b
C
55 $scope.status = 'default';
56 $scope.loading = false;
57 $scope.controls = {};
f28caa2c 58 $scope.langs = ['php', 'js', 'ang', 'cli'];
c2d3af50 59 $scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
f28caa2c
CW
60 $scope.code = {
61 php: [
62 {name: 'oop', label: ts('OOP Style'), code: ''},
63 {name: 'php', label: ts('Traditional'), code: ''}
64 ],
65 js: [
66 {name: 'js', label: ts('Single Call'), code: ''},
67 {name: 'js2', label: ts('Batch Calls'), code: ''}
68 ],
69 ang: [
70 {name: 'ang', label: ts('Single Call'), code: ''},
71 {name: 'ang2', label: ts('Batch Calls'), code: ''}
72 ],
73 cli: [
74 {name: 'cv', label: ts('CV'), code: ''}
75 ]
76 };
19b53e5b
C
77
78 if (!entities.length) {
449c4e6b 79 formatForSelect2(schema, entities, 'name', ['description', 'icon']);
19b53e5b
C
80 }
81
82 $scope.$bindToRoute({
83 expr: 'index',
84 param: 'index',
85 default: ''
86 });
87
88 function ucfirst(str) {
89 return str[0].toUpperCase() + str.slice(1);
90 }
91
92 function lcfirst(str) {
93 return str[0].toLowerCase() + str.slice(1);
94 }
95
96 function pluralize(str) {
628ae369
CW
97 var lastLetter = str[str.length - 1],
98 lastTwo = str[str.length - 2] + lastLetter;
99 if (lastLetter === 's' || lastTwo === 'ch') {
100 return str + 'es';
19b53e5b 101 }
628ae369
CW
102 if (lastLetter === 'y' && lastTwo !== 'ey') {
103 return str.slice(0, -1) + 'ies';
104 }
105 return str + 's';
19b53e5b
C
106 }
107
19b53e5b
C
108 // Reformat an existing array of objects for compatibility with select2
109 function formatForSelect2(input, container, key, extra, prefix) {
110 _.each(input, function(item) {
111 var id = (prefix || '') + item[key];
112 var formatted = {id: id, text: id};
113 if (extra) {
114 _.merge(formatted, _.pick(item, extra));
115 }
116 container.push(formatted);
117 });
118 return container;
119 }
120
f54beb1e
CW
121 // Replaces contents of fieldList array with current fields formatted for select2
122 function getFieldList(fieldList, action, addPseudoconstant) {
123 var fieldInfo = _.cloneDeep(_.findWhere(getEntity().actions, {name: action}).fields);
124 fieldList.length = 0;
37d82abe 125 if (addPseudoconstant) {
37d82abe
CW
126 addPseudoconstants(fieldInfo, addPseudoconstant);
127 }
f54beb1e 128 formatForSelect2(fieldInfo, fieldList, 'name', ['description', 'required', 'default_value']);
19b53e5b
C
129 }
130
37d82abe
CW
131 // Note: this function expects fieldList to be select2-formatted already
132 function addJoins(fieldList, addWildcard, addPseudoconstant) {
f54beb1e
CW
133 // Add entities specified by the join param
134 _.each(getExplicitJoins(), function(joinEntity, joinAlias) {
135 var wildCard = addWildcard ? [{id: joinAlias + '.*', text: joinAlias + '.*', 'description': 'All core ' + joinEntity + ' fields'}] : [],
136 joinFields = _.cloneDeep(entityFields(joinEntity));
137 if (joinFields) {
138 if (addPseudoconstant) {
139 addPseudoconstants(joinFields, addPseudoconstant);
140 }
141 fieldList.push({
142 text: joinEntity + ' AS ' + joinAlias,
143 description: 'Explicit join to ' + joinEntity,
144 children: wildCard.concat(formatForSelect2(joinFields, [], 'name', ['description'], joinAlias + '.'))
145 });
146 }
147 });
148 // Add implicit joins based on schema links
7d91265c 149 _.each(links[$scope.entity], function(link) {
39e0f675
CW
150 var linkFields = _.cloneDeep(entityFields(link.entity)),
151 wildCard = addWildcard ? [{id: link.alias + '.*', text: link.alias + '.*', 'description': 'All core ' + link.entity + ' fields'}] : [];
19b53e5b 152 if (linkFields) {
37d82abe
CW
153 if (addPseudoconstant) {
154 addPseudoconstants(linkFields, addPseudoconstant);
155 }
f54beb1e 156 fieldList.push({
19b53e5b 157 text: link.alias,
c2d3af50 158 description: 'Implicit join to ' + link.entity,
39e0f675 159 children: wildCard.concat(formatForSelect2(linkFields, [], 'name', ['description'], link.alias + '.'))
19b53e5b
C
160 });
161 }
162 });
19b53e5b
C
163 }
164
37d82abe
CW
165 // Note: this function transforms a raw list a-la getFields; not a select2-formatted list
166 function addPseudoconstants(fieldList, toAdd) {
167 var optionFields = _.filter(fieldList, 'options');
168 _.each(optionFields, function(field) {
169 var pos = _.findIndex(fieldList, {name: field.name}) + 1;
170 _.each(toAdd, function(suffix) {
171 var newField = _.cloneDeep(field);
172 newField.name += ':' + suffix;
173 fieldList.splice(pos, 0, newField);
174 });
175 });
176 }
177
136ca5bb
CW
178 $scope.help = function(title, content) {
179 if (!content) {
19b53e5b
C
180 $scope.helpTitle = helpTitle;
181 $scope.helpContent = helpContent;
182 } else {
183 $scope.helpTitle = title;
fc95d9a5 184 $scope.helpContent = formatHelp(content);
19b53e5b
C
185 }
186 };
187
136ca5bb
CW
188 // Sets the static help text (which gets overridden by mousing over other elements)
189 function setHelp(title, content) {
190 $scope.helpTitle = helpTitle = title;
fc95d9a5 191 $scope.helpContent = helpContent = formatHelp(content);
136ca5bb
CW
192 }
193
fc95d9a5
CW
194 // Convert plain-text help to markdown; replace variables and format links
195 function formatHelp(rawContent) {
196 function formatRefs(see) {
197 _.each(see, function(ref, idx) {
198 var match = ref.match(/^\\Civi\\Api4\\([a-zA-Z]+)$/);
199 if (match) {
200 ref = '#/explorer/' + match[1];
201 }
202 if (ref[0] === '\\') {
203 ref = 'https://github.com/civicrm/civicrm-core/blob/master' + ref.replace(/\\/i, '/') + '.php';
204 }
205 see[idx] = '<a target="' + (ref[0] === '#' ? '_self' : '_blank') + '" href="' + ref + '">' + see[idx] + '</a>';
206 });
207 }
136ca5bb
CW
208 var formatted = _.cloneDeep(rawContent);
209 if (formatted.description) {
210 formatted.description = marked(formatted.description);
211 }
212 if (formatted.comment) {
213 formatted.comment = marked(formatted.comment);
214 }
fc95d9a5 215 formatRefs(formatted.see);
136ca5bb
CW
216 return formatted;
217 }
218
19b53e5b
C
219 $scope.fieldHelp = function(fieldName) {
220 var field = getField(fieldName, $scope.entity, $scope.action);
221 if (!field) {
222 return;
223 }
224 var info = {
225 description: field.description,
226 type: field.data_type
227 };
228 if (field.default_value) {
229 info.default = field.default_value;
230 }
231 if (field.required_if) {
232 info.required_if = field.required_if;
233 } else if (field.required) {
234 info.required = 'true';
235 }
236 return info;
237 };
238
37d82abe 239 // Returns field list for write params (values, defaults)
bb3786d2
CW
240 $scope.fieldList = function(param) {
241 return function() {
f54beb1e
CW
242 var fields = [];
243 getFieldList(fields, $scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name']);
bb3786d2
CW
244 // Disable fields that are already in use
245 _.each($scope.params[param] || [], function(val) {
37d82abe
CW
246 var usedField = val[0].replace(':name', '');
247 (_.findWhere(fields, {id: usedField}) || {}).disabled = true;
248 (_.findWhere(fields, {id: usedField + ':name'}) || {}).disabled = true;
bb3786d2
CW
249 });
250 return {results: fields};
251 };
19b53e5b
C
252 };
253
254 $scope.formatSelect2Item = function(row) {
255 return _.escape(row.text) +
256 (row.required ? '<span class="crm-marker"> *</span>' : '') +
257 (row.description ? '<div class="crm-select2-row-description"><p>' + _.escape(row.description) + '</p></div>' : '');
258 };
259
a26e006b
CW
260 $scope.clearParam = function(name, idx) {
261 if (typeof idx === 'undefined') {
262 $scope.params[name] = $scope.availableParams[name].default;
263 } else {
264 $scope.params[name].splice(idx, 1);
265 }
19b53e5b
C
266 };
267
f0acec37
CW
268 // Gets params that should be represented as generic input fields in the explorer
269 // This fn doesn't have to be particularly efficient as its output is cached in one-time bindings
270 $scope.getGenericParams = function(paramType, defaultNull) {
271 // Returns undefined if params are not yet set; one-time bindings will stabilize when this function returns a value
272 if (_.isEmpty($scope.availableParams)) {
273 return;
274 }
c2d3af50 275 var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy', 'having', 'join'];
f0acec37
CW
276 if ($scope.availableParams.limit && $scope.availableParams.offset) {
277 specialParams.push('limit', 'offset');
278 }
279 return _.transform($scope.availableParams, function(genericParams, param, name) {
280 if (!_.contains(specialParams, name) &&
281 !(typeof paramType !== 'undefined' && !_.contains(paramType, param.type[0])) &&
282 !(typeof defaultNull !== 'undefined' && ((param.default === null) !== defaultNull))
283 ) {
284 genericParams[name] = param;
285 }
286 });
19b53e5b
C
287 };
288
289 $scope.selectRowCount = function() {
651c4c95
CW
290 var index = params.select.indexOf('row_count');
291 if (index < 0) {
292 $scope.params.select.push('row_count');
19b53e5b 293 } else {
651c4c95 294 $scope.params.select.splice(index, 1);
19b53e5b
C
295 }
296 };
297
298 $scope.isSelectRowCount = function() {
b568c26c 299 return isSelectRowCount($scope.params);
19b53e5b
C
300 };
301
f28caa2c
CW
302 $scope.selectLang = function(lang) {
303 $scope.selectedTab.code = lang;
304 writeCode();
305 };
306
b568c26c 307 function isSelectRowCount(params) {
651c4c95 308 return params && params.select && params.select.indexOf('row_count') >= 0;
b568c26c
CW
309 }
310
19b53e5b
C
311 function getEntity(entityName) {
312 return _.findWhere(schema, {name: entityName || $scope.entity});
313 }
314
315 // Get all params that have been set
316 function getParams() {
317 var params = {};
318 _.each($scope.params, function(param, key) {
319 if (param != $scope.availableParams[key].default && !(typeof param === 'object' && _.isEmpty(param))) {
320 if (_.contains($scope.availableParams[key].type, 'array') && (typeof objectParams[key] === 'undefined')) {
6ba6f2bd 321 params[key] = parseYaml(JSON.parse(angular.toJson(param)));
19b53e5b
C
322 } else {
323 params[key] = param;
324 }
325 }
326 });
327 _.each(objectParams, function(defaultVal, key) {
328 if (params[key]) {
329 var newParam = {};
330 _.each(params[key], function(item) {
cddf293f
CW
331 var val = _.cloneDeep(item[1]);
332 // Remove blank items from "chain" array
333 if (_.isArray(val)) {
334 _.eachRight(item[1], function(v, k) {
335 if (v) {
336 return false;
337 }
338 val.length--;
339 });
340 }
341 newParam[item[0]] = parseYaml(val);
19b53e5b
C
342 });
343 params[key] = newParam;
344 }
345 });
346 return params;
347 }
348
349 function parseYaml(input) {
f54beb1e
CW
350 if (typeof input === 'undefined' || input === '') {
351 return input;
19b53e5b 352 }
f54beb1e
CW
353 // Return literal quoted string without removing quotes - for the sake of JOIN ON clauses
354 if (_.isString(input) && input[0] === input[input.length - 1] && _.includes(["'", '"'], input[0])) {
355 return input;
6ba6f2bd 356 }
19b53e5b
C
357 if (_.isObject(input) || _.isArray(input)) {
358 _.each(input, function(item, index) {
359 input[index] = parseYaml(item);
360 });
361 return input;
362 }
363 try {
364 var output = (input === '>') ? '>' : jsyaml.safeLoad(input);
365 // We don't want dates parsed to js objects
366 return _.isDate(output) ? input : output;
367 } catch (e) {
368 return input;
369 }
370 }
371
f54beb1e
CW
372 this.buildFieldList = function() {
373 var actionInfo = _.findWhere(actions, {id: $scope.action});
374 getFieldList($scope.fields, $scope.action);
375 getFieldList($scope.fieldsAndJoins, $scope.action, ['name']);
376 getFieldList($scope.fieldsAndJoinsAndFunctions, $scope.action);
377 getFieldList($scope.fieldsAndJoinsAndFunctionsWithSuffixes, $scope.action, ['name', 'label']);
378 getFieldList($scope.fieldsAndJoinsAndFunctionsAndWildcards, $scope.action, ['name', 'label']);
379 if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
380 addJoins($scope.fieldsAndJoins);
381 // SQL functions are supported if HAVING is
382 if (actionInfo.params.having) {
383 var functions = {
384 text: ts('FUNCTION'),
385 description: ts('Calculate result of a SQL function'),
386 children: _.transform(CRM.vars.api4.functions, function(result, fn) {
387 result.push({
388 id: fn.name + '() AS ' + fn.name.toLowerCase(),
389 text: fn.name + '()',
390 description: fn.name + '(' + describeSqlFn(fn.params) + ')'
391 });
392 })
393 };
394 $scope.fieldsAndJoinsAndFunctions.push(functions);
395 $scope.fieldsAndJoinsAndFunctionsWithSuffixes.push(functions);
396 $scope.fieldsAndJoinsAndFunctionsAndWildcards.push(functions);
397 }
398 addJoins($scope.fieldsAndJoinsAndFunctions, true);
399 addJoins($scope.fieldsAndJoinsAndFunctionsWithSuffixes, false, ['name', 'label']);
400 addJoins($scope.fieldsAndJoinsAndFunctionsAndWildcards, true, ['name', 'label']);
401 }
402 $scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
403 };
404
19b53e5b
C
405 function selectAction() {
406 $scope.action = $routeParams.api4action;
19b53e5b
C
407 if (!actions.length) {
408 formatForSelect2(getEntity().actions, actions, 'name', ['description', 'params']);
409 }
410 if ($scope.action) {
411 var actionInfo = _.findWhere(actions, {id: $scope.action});
19b53e5b
C
412 _.each(actionInfo.params, function (param, name) {
413 var format,
414 defaultVal = _.cloneDeep(param.default);
415 if (param.type) {
416 switch (param.type[0]) {
417 case 'int':
418 case 'bool':
419 format = param.type[0];
420 break;
421
422 case 'array':
423 case 'object':
424 format = 'json';
425 break;
426
427 default:
428 format = 'raw';
429 }
9ebb0bb9 430 if (name === 'limit') {
19b53e5b
C
431 defaultVal = 25;
432 }
9ebb0bb9
CW
433 if (name === 'debug') {
434 defaultVal = true;
435 }
19b53e5b
C
436 if (name === 'values') {
437 defaultVal = defaultValues(defaultVal);
438 }
bb6bfd68
CW
439 if (name === 'loadOptions' && $scope.action === 'getFields') {
440 param.options = [
441 false,
442 true,
443 ['id', 'name', 'label'],
444 ['id', 'name', 'label', 'abbr', 'description', 'color', 'icon']
445 ];
446 format = 'json';
447 defaultVal = false;
448 param.type = ['string'];
449 }
19b53e5b
C
450 $scope.$bindToRoute({
451 expr: 'params["' + name + '"]',
452 param: name,
453 format: format,
454 default: defaultVal,
455 deep: format === 'json'
456 });
457 }
f0acec37
CW
458 if (typeof objectParams[name] !== 'undefined' && name !== 'orderBy') {
459 $scope.$watch('params.' + name, function (values) {
19b53e5b 460 // Remove empty values
a26e006b 461 _.each(values, function (clause, index) {
19b53e5b 462 if (!clause || !clause[0]) {
a26e006b 463 $scope.clearParam(name, index);
19b53e5b
C
464 }
465 });
466 }, true);
a26e006b 467 }
9f6c0e4f
CW
468 if (name === 'select' && actionInfo.params.having) {
469 $scope.$watchCollection('params.select', function(values) {
470 $scope.havingOptions.length = 0;
471 _.each(values, function(item) {
472 var pieces = item.split(' AS '),
37d82abe 473 alias = _.trim(pieces[pieces.length - 1]).replace(':label', ':name');
9f6c0e4f
CW
474 $scope.havingOptions.push({id: alias, text: alias});
475 });
476 });
477 }
c2d3af50 478 if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select' || name === 'join') {
19b53e5b
C
479 $scope.$watch('controls.' + name, function(value) {
480 var field = value;
481 $timeout(function() {
482 if (field) {
c2d3af50 483 if (name === 'join') {
af6f5ac8 484 $scope.params[name].push([field + ' AS ' + _.snakeCase(field), false]);
f54beb1e 485 ctrl.buildFieldList();
c2d3af50
CW
486 }
487 else if (typeof objectParams[name] === 'undefined') {
f0acec37
CW
488 $scope.params[name].push(field);
489 } else {
490 var defaultOp = _.cloneDeep(objectParams[name]);
491 if (name === 'chain') {
492 var num = $scope.params.chain.length;
493 defaultOp[0] = field;
494 field = 'name_me_' + num;
495 }
496 $scope.params[name].push([field, defaultOp]);
19b53e5b 497 }
19b53e5b
C
498 $scope.controls[name] = null;
499 }
500 });
501 });
502 }
503 });
f54beb1e 504 ctrl.buildFieldList();
19b53e5b
C
505 $scope.availableParams = actionInfo.params;
506 }
507 writeCode();
508 }
509
a26e006b
CW
510 function describeSqlFn(params) {
511 var desc = ' ';
512 _.each(params, function(param) {
513 desc += ' ';
514 if (param.prefix) {
515 desc += _.filter(param.prefix).join('|') + ' ';
516 }
517 if (param.expr === 1) {
518 desc += 'expr ';
519 } else if (param.expr > 1) {
520 desc += 'expr, ... ';
521 }
522 if (param.suffix) {
523 desc += ' ' + _.filter(param.suffix).join('|') + ' ';
524 }
525 });
526 return desc.replace(/[ ]+/g, ' ');
527 }
528
19b53e5b
C
529 function defaultValues(defaultVal) {
530 _.each($scope.fields, function(field) {
531 if (field.required) {
532 defaultVal.push([field.id, '']);
533 }
534 });
535 return defaultVal;
536 }
537
538 function stringify(value, trim) {
539 if (typeof value === 'undefined') {
540 return '';
541 }
542 var str = JSON.stringify(value).replace(/,/g, ', ');
543 if (trim) {
544 str = str.slice(1, -1);
545 }
546 return str.trim();
547 }
548
549 function writeCode() {
19d01932 550 var code = {},
19b53e5b
C
551 entity = $scope.entity,
552 action = $scope.action,
553 params = getParams(),
2a68b84a 554 index = isInt($scope.index) ? +$scope.index : parseYaml($scope.index),
19b53e5b
C
555 result = 'result';
556 if ($scope.entity && $scope.action) {
9ebb0bb9 557 delete params.debug;
19b53e5b
C
558 if (action.slice(0, 3) === 'get') {
559 result = entity.substr(0, 7) === 'Custom_' ? _.camelCase(entity.substr(7)) : entity;
560 result = lcfirst(action.replace(/s$/, '').slice(3) || result);
561 }
562 var results = lcfirst(_.isNumber(index) ? result : pluralize(result)),
563 paramCount = _.size(params),
19b53e5b
C
564 i = 0;
565
f28caa2c
CW
566 switch ($scope.selectedTab.code) {
567 case 'js':
568 case 'ang':
569 // Write javascript
570 var js = "'" + entity + "', '" + action + "', {";
571 _.each(params, function(param, key) {
572 js += "\n " + key + ': ' + stringify(param) +
573 (++i < paramCount ? ',' : '');
574 if (key === 'checkPermissions') {
575 js += ' // IGNORED: permissions are always enforced from client-side requests';
576 }
577 });
578 js += "\n}";
579 if (index || index === 0) {
580 js += ', ' + JSON.stringify(index);
581 }
582 code.js = "CRM.api4(" + js + ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
583 code.js2 = "CRM.api4({" + results + ': [' + js + "]}).then(function(batch) {\n // do something with batch." + results + " array\n}, function(failure) {\n // handle failure\n});";
584 code.ang = "crmApi4(" + js + ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
585 code.ang2 = "crmApi4({" + results + ': [' + js + "]}).then(function(batch) {\n // do something with batch." + results + " array\n}, function(failure) {\n // handle failure\n});";
586 break;
587
588 case 'php':
589 // Write php code
590 code.php = '$' + results + " = civicrm_api4('" + entity + "', '" + action + "', [";
591 _.each(params, function(param, key) {
592 code.php += "\n '" + key + "' => " + phpFormat(param, 4) + ',';
593 });
594 code.php += "\n]";
595 if (index || index === 0) {
596 code.php += ', ' + phpFormat(index);
597 }
598 code.php += ");";
599
600 // Write oop code
601 code.oop = '$' + results + " = " + formatOOP(entity, action, params, 2) + "\n ->execute()";
651c4c95 602 if (_.isNumber(index)) {
f28caa2c
CW
603 code.oop += !index ? '\n ->first()' : (index === -1 ? '\n ->last()' : '\n ->itemAt(' + index + ')');
604 } else if (index) {
605 if (_.isString(index) || (_.isPlainObject(index) && !index[0] && !index['0'])) {
606 code.oop += "\n ->indexBy('" + (_.isPlainObject(index) ? _.keys(index)[0] : index) + "')";
607 }
608 if (_.isArray(index) || _.isPlainObject(index)) {
609 code.oop += "\n ->column('" + (_.isArray(index) ? index[0] : _.values(index)[0]) + "')";
610 }
611 }
612 code.oop += ";\n";
651c4c95 613 if (!_.isNumber(index)) {
f28caa2c
CW
614 code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n // do something\n}';
615 }
616 break;
3b1f7ce7 617
f28caa2c
CW
618 case 'cli':
619 // Write cli code
620 code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
19b53e5b 621 }
19b53e5b 622 }
19d01932 623 _.each($scope.code, function(vals) {
f28caa2c 624 _.each(vals, function(style) {
19d01932
CW
625 style.code = code[style.name] ? prettyPrintOne(code[style.name]) : '';
626 });
19b53e5b
C
627 });
628 }
629
b568c26c
CW
630 // Format oop params
631 function formatOOP(entity, action, params, indent) {
632 var code = '',
633 newLine = "\n" + _.repeat(' ', indent);
634 if (entity.substr(0, 7) !== 'Custom_') {
635 code = "\\Civi\\Api4\\" + entity + '::' + action + '()';
636 } else {
637 code = "\\Civi\\Api4\\CustomValue::" + action + "('" + entity.substr(7) + "')";
638 }
639 _.each(params, function(param, key) {
640 var val = '';
641 if (typeof objectParams[key] !== 'undefined' && key !== 'chain') {
642 _.each(param, function(item, index) {
643 val = phpFormat(index) + ', ' + phpFormat(item, 2 + indent);
644 code += newLine + "->add" + ucfirst(key).replace(/s$/, '') + '(' + val + ')';
645 });
646 } else if (key === 'where') {
647 _.each(param, function (clause) {
648 if (clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT') {
649 code += newLine + "->addClause(" + phpFormat(clause[0]) + ", " + phpFormat(clause[1]).slice(1, -1) + ')';
650 } else {
651 code += newLine + "->addWhere(" + phpFormat(clause).slice(1, -1) + ")";
652 }
653 });
654 } else if (key === 'select') {
651c4c95
CW
655 // selectRowCount() is a shortcut for addSelect('row_count')
656 if (isSelectRowCount(params)) {
657 code += newLine + '->selectRowCount()';
658 param = _.without(param, 'row_count');
659 }
660 // addSelect() is a variadic function & can take multiple arguments
661 if (param.length) {
662 code += newLine + '->addSelect(' + phpFormat(param).slice(1, -1) + ')';
663 }
b568c26c
CW
664 } else if (key === 'chain') {
665 _.each(param, function(chain, name) {
666 code += newLine + "->addChain('" + name + "', " + formatOOP(chain[0], chain[1], chain[2], 2 + indent);
667 code += (chain.length > 3 ? ',' : '') + (!_.isEmpty(chain[2]) ? newLine : ' ') + (chain.length > 3 ? phpFormat(chain[3]) : '') + ')';
668 });
669 }
670 else {
671 code += newLine + "->set" + ucfirst(key) + '(' + phpFormat(param, 2 + indent) + ')';
672 }
673 });
674 return code;
675 }
676
19b53e5b
C
677 function isInt(value) {
678 if (_.isFinite(value)) {
679 return true;
680 }
681 if (!_.isString(value)) {
682 return false;
683 }
684 return /^-{0,1}\d+$/.test(value);
685 }
686
687 function formatMeta(resp) {
688 var ret = '';
689 _.each(resp, function(val, key) {
690 if (key !== 'values' && !_.isPlainObject(val) && !_.isFunction(val)) {
691 ret += (ret.length ? ', ' : '') + key + ': ' + (_.isArray(val) ? '[' + val + ']' : val);
692 }
693 });
3b1f7ce7 694 return prettyPrintOne(_.escape(ret));
19b53e5b
C
695 }
696
697 $scope.execute = function() {
2aafb0fc 698 $scope.status = 'info';
19b53e5b 699 $scope.loading = true;
2e40130b 700 $http.post(CRM.url('civicrm/ajax/api4/' + $scope.entity + '/' + $scope.action, {
19b53e5b 701 params: angular.toJson(getParams()),
2a68b84a 702 index: isInt($scope.index) ? +$scope.index : parseYaml($scope.index)
2e40130b 703 }), null, {
ea3acfee
SL
704 headers: {
705 'X-Requested-With': 'XMLHttpRequest'
706 }
707 }).then(function(resp) {
19b53e5b 708 $scope.loading = false;
2aafb0fc 709 $scope.status = resp.data && resp.data.debug && resp.data.debug.log ? 'warning' : 'success';
b65fa6dc 710 $scope.debug = debugFormat(resp.data);
651c4c95
CW
711 $scope.result = [
712 formatMeta(resp.data),
713 prettyPrintOne('(' + resp.data.values.length + ') ' + _.escape(JSON.stringify(resp.data.values, null, 2)), 'js', 1)
714 ];
19b53e5b
C
715 }, function(resp) {
716 $scope.loading = false;
717 $scope.status = 'danger';
b65fa6dc 718 $scope.debug = debugFormat(resp.data);
651c4c95
CW
719 $scope.result = [
720 formatMeta(resp),
721 prettyPrintOne(_.escape(JSON.stringify(resp.data, null, 2)))
722 ];
19b53e5b
C
723 });
724 };
725
b65fa6dc
CW
726 function debugFormat(data) {
727 var debug = data.debug ? prettyPrintOne(_.escape(JSON.stringify(data.debug, null, 2)).replace(/\\n/g, "\n")) : null;
728 delete data.debug;
729 return debug;
730 }
731
19b53e5b
C
732 /**
733 * Format value to look like php code
734 */
735 function phpFormat(val, indent) {
736 if (typeof val === 'undefined') {
737 return '';
738 }
6ba6f2bd
CW
739 if (val === null || val === true || val === false) {
740 return JSON.stringify(val).toUpperCase();
741 }
19b53e5b
C
742 indent = (typeof indent === 'number') ? _.repeat(' ', indent) : (indent || '');
743 var ret = '',
744 baseLine = indent ? indent.slice(0, -2) : '',
cddf293f
CW
745 newLine = indent ? '\n' : '',
746 trailingComma = indent ? ',' : '';
19b53e5b
C
747 if ($.isPlainObject(val)) {
748 $.each(val, function(k, v) {
749 ret += (ret ? ', ' : '') + newLine + indent + "'" + k + "' => " + phpFormat(v);
750 });
cddf293f 751 return '[' + ret + trailingComma + newLine + baseLine + ']';
19b53e5b
C
752 }
753 if ($.isArray(val)) {
754 $.each(val, function(k, v) {
755 ret += (ret ? ', ' : '') + newLine + indent + phpFormat(v);
756 });
cddf293f 757 return '[' + ret + trailingComma + newLine + baseLine + ']';
19b53e5b
C
758 }
759 if (_.isString(val) && !_.contains(val, "'")) {
760 return "'" + val + "'";
761 }
762 return JSON.stringify(val).replace(/\$/g, '\\$');
763 }
764
765 function fetchMeta() {
766 crmApi4(getMetaParams)
767 .then(function(data) {
768 if (data.actions) {
769 getEntity().actions = data.actions;
770 selectAction();
771 }
772 });
773 }
774
775 // Help for an entity with no action selected
776 function showEntityHelp(entityName) {
777 var entityInfo = getEntity(entityName);
136ca5bb 778 setHelp($scope.entity, {
19b53e5b 779 description: entityInfo.description,
0493ec47
CW
780 comment: entityInfo.comment,
781 see: entityInfo.see
136ca5bb 782 });
19b53e5b
C
783 }
784
785 if (!$scope.entity) {
136ca5bb 786 setHelp(ts('APIv4 Explorer'), {description: docs.description, comment: docs.comment, see: docs.see});
19b53e5b
C
787 } else if (!actions.length && !getEntity().actions) {
788 getMetaParams.actions = [$scope.entity, 'getActions', {chain: {fields: [$scope.entity, 'getFields', {action: '$name'}]}}];
789 fetchMeta();
790 } else {
791 selectAction();
792 }
793
794 if ($scope.entity) {
795 showEntityHelp($scope.entity);
796 }
797
798 // Update route when changing entity
799 $scope.$watch('entity', function(newVal, oldVal) {
800 if (oldVal !== newVal) {
801 // Flush actions cache to re-fetch for new entity
802 actions = [];
803 $location.url('/explorer/' + newVal);
804 }
805 });
806
807 // Update route when changing actions
808 $scope.$watch('action', function(newVal, oldVal) {
809 if ($scope.entity && $routeParams.api4action !== newVal && !_.isUndefined(newVal)) {
810 $location.url('/explorer/' + $scope.entity + '/' + newVal);
811 } else if (newVal) {
136ca5bb 812 setHelp($scope.entity + '::' + newVal, _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment', 'see']));
19b53e5b
C
813 }
814 });
815
9cea3619
CW
816 $scope.paramDoc = function(name) {
817 return docs.params[name];
19b53e5b
C
818 };
819
2c5d5bca
CW
820 $scope.executeDoc = function() {
821 var doc = {
822 description: ts('Runs API call on the CiviCRM database.'),
823 comment: ts('Results and debugging info will be displayed below.')
824 };
825 if ($scope.action === 'delete') {
826 doc.WARNING = ts('This API call will be executed on the real database. Deleting data cannot be undone.');
827 }
828 else if ($scope.action && $scope.action.slice(0, 3) !== 'get') {
829 doc.WARNING = ts('This API call will be executed on the real database. It cannot be undone.');
830 }
831 return doc;
832 };
833
834 $scope.saveDoc = function() {
835 return {
836 description: ts('Save API call as a smart group.'),
48102254
CW
837 comment: ts('Create a SavedSearch using these API params to populate a smart group.') +
838 '\n\n' + ts('NOTE: you must select contact id as the only field.')
2c5d5bca
CW
839 };
840 };
841
19b53e5b
C
842 $scope.$watch('params', writeCode, true);
843 $scope.$watch('index', writeCode);
844 writeCode();
845
4e97c268 846 $scope.save = function() {
48102254
CW
847 $scope.params.limit = $scope.params.offset = 0;
848 if ($scope.params.chain.length) {
849 CRM.alert(ts('Smart groups are not compatible with API chaining.'), ts('Error'), 'error', {expires: 5000});
850 return;
851 }
852 if ($scope.params.select.length !== 1 || !_.includes($scope.params.select[0], 'id')) {
853 CRM.alert(ts('To create a smart group, the API must select contact id and no other fields.'), ts('Error'), 'error', {expires: 5000});
854 return;
855 }
4e97c268
CW
856 var model = {
857 title: '',
d7507e89
CW
858 description: '',
859 visibility: 'User and User Admin Only',
860 group_type: [],
4e97c268
CW
861 id: null,
862 entity: $scope.entity,
863 params: JSON.parse(angular.toJson($scope.params))
864 };
865 model.params.version = 4;
4e97c268
CW
866 delete model.params.chain;
867 delete model.params.debug;
868 delete model.params.limit;
48102254
CW
869 delete model.params.offset;
870 delete model.params.orderBy;
4e97c268
CW
871 delete model.params.checkPermissions;
872 var options = CRM.utils.adjustDialogDefaults({
873 width: '500px',
874 autoOpen: false,
875 title: ts('Save smart group')
876 });
877 dialogService.open('saveSearchDialog', '~/api4Explorer/SaveSearch.html', model, options);
878 };
879 });
880
881 angular.module('api4Explorer').controller('SaveSearchCtrl', function($scope, crmApi4, dialogService) {
882 var ts = $scope.ts = CRM.ts(),
883 model = $scope.model;
d7507e89
CW
884 $scope.groupEntityRefParams = {
885 entity: 'Group',
886 api: {
887 params: {is_hidden: 0, is_active: 1, 'saved_search_id.api_entity': model.entity},
888 extra: ['saved_search_id', 'description', 'visibility', 'group_type']
889 },
890 select: {
891 allowClear: true,
892 minimumInputLength: 0,
893 placeholder: ts('Select existing group')
894 }
895 };
896 if (!CRM.checkPerm('administer reserved groups')) {
897 $scope.groupEntityRefParams.api.params.is_reserved = 0;
898 }
899 $scope.perm = {
900 administerReservedGroups: CRM.checkPerm('administer reserved groups')
901 };
902 $scope.options = CRM.vars.api4.groupOptions;
4e97c268
CW
903 $scope.$watch('model.id', function(id) {
904 if (id) {
d7507e89 905 _.assign(model, $('#api-save-search-select-group').select2('data').extra);
4e97c268
CW
906 }
907 });
908 $scope.cancel = function() {
909 dialogService.cancel('saveSearchDialog');
910 };
911 $scope.save = function() {
912 $('.ui-dialog:visible').block();
913 var group = model.id ? {id: model.id} : {title: model.title};
914 group.description = model.description;
d7507e89
CW
915 group.visibility = model.visibility;
916 group.group_type = model.group_type;
4e97c268
CW
917 group.saved_search_id = '$id';
918 var savedSearch = {
919 api_entity: model.entity,
920 api_params: model.params
921 };
922 if (group.id) {
d7507e89 923 savedSearch.id = model.saved_search_id;
4e97c268
CW
924 }
925 crmApi4('SavedSearch', 'save', {records: [savedSearch], chain: {group: ['Group', 'save', {'records': [group]}]}})
926 .then(function(result) {
927 dialogService.close('saveSearchDialog', result[0]);
928 });
929 };
19b53e5b
C
930 });
931
af6f5ac8 932 angular.module('api4Explorer').directive('crmApi4Clause', function() {
19b53e5b
C
933 return {
934 scope: {
af6f5ac8 935 data: '<crmApi4Clause'
19b53e5b 936 },
24463df7 937 templateUrl: '~/api4Explorer/Clause.html',
af6f5ac8 938 controller: function ($scope, $element, $timeout) {
f54beb1e
CW
939 var ts = $scope.ts = CRM.ts(),
940 ctrl = $scope.$ctrl = this;
af6f5ac8
CW
941 this.conjunctions = {AND: ts('And'), OR: ts('Or'), NOT: ts('Not')};
942 this.operators = CRM.vars.api4.operators;
943 this.sortOptions = {
944 axis: 'y',
945 connectWith: '.api4-clause-group-sortable',
946 containment: $element.closest('.api4-clause-fieldset'),
947 over: onSortOver,
948 start: onSort,
949 stop: onSort
950 };
19b53e5b 951
af6f5ac8
CW
952 this.addGroup = function(op) {
953 $scope.data.clauses.push([op, []]);
19b53e5b
C
954 };
955
af6f5ac8
CW
956 this.removeGroup = function() {
957 $scope.data.groupParent.splice($scope.data.groupIndex, 1);
19b53e5b
C
958 };
959
af6f5ac8
CW
960 function onSort(event, ui) {
961 $($element).closest('.api4-clause-fieldset').toggleClass('api4-sorting', event.type === 'sortstart');
19b53e5b 962 $('.api4-input.form-inline').css('margin-left', '');
af6f5ac8 963 }
19b53e5b
C
964
965 // Indent clause while dragging between nested groups
af6f5ac8 966 function onSortOver(event, ui) {
19b53e5b
C
967 var offset = 0;
968 if (ui.sender) {
969 offset = $(ui.placeholder).offset().left - $(ui.sender).offset().left;
970 }
971 $('.api4-input.form-inline.ui-sortable-helper').css('margin-left', '' + offset + 'px');
af6f5ac8 972 }
19b53e5b 973
af6f5ac8 974 this.addClause = function() {
19b53e5b 975 $timeout(function() {
af6f5ac8
CW
976 if (ctrl.newClause) {
977 $scope.data.clauses.push([ctrl.newClause, '=', '']);
978 ctrl.newClause = null;
19b53e5b
C
979 }
980 });
af6f5ac8
CW
981 };
982 $scope.$watch('data.clauses', function(values) {
983 // Iterate in reverse order so index doesn't get out-of-sync during splice
984 _.forEachRight(values, function(clause, index) {
985 // Remove empty values
986 if (index >= ($scope.data.skip || 0)) {
987 if (typeof clause !== 'undefined' && !clause[0]) {
988 values.splice(index, 1);
989 }
990 // Add/remove value if operator allows for one
991 else if (typeof clause[1] === 'string' && _.contains(clause[1], 'NULL')) {
992 clause.length = 2;
993 } else if (typeof clause[1] === 'string' && clause.length === 2) {
994 clause.push('');
995 }
6ba6f2bd 996 }
19b53e5b
C
997 });
998 }, true);
999 }
1000 };
1001 });
1002
1003 angular.module('api4Explorer').directive('api4ExpValue', function($routeParams, crmApi4) {
1004 return {
1005 scope: {
1006 data: '=api4ExpValue'
1007 },
1008 require: 'ngModel',
1009 link: function (scope, element, attrs, ctrl) {
6f97b1d9 1010 var ts = scope.ts = CRM.ts(),
6872a653 1011 multi = _.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], scope.data.op),
19b53e5b 1012 entity = $routeParams.api4entity,
c752d94b 1013 action = scope.data.action || $routeParams.api4action;
19b53e5b
C
1014
1015 function destroyWidget() {
1016 var $el = $(element);
1017 if ($el.is('.crm-form-date-wrapper .crm-hidden-date')) {
1018 $el.crmDatepicker('destroy');
1019 }
1020 if ($el.is('.select2-container + input')) {
1021 $el.crmEntityRef('destroy');
1022 }
1023 $(element).removeData().removeAttr('type').removeAttr('placeholder').show();
1024 }
1025
1026 function makeWidget(field, op) {
1027 var $el = $(element),
bc356925 1028 inputType = field.input_type,
19b53e5b
C
1029 dataType = field.data_type;
1030 if (!op) {
1031 op = field.serialize || dataType === 'Array' ? 'IN' : '=';
1032 }
6872a653 1033 multi = _.includes(['IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'], op);
19b53e5b
C
1034 if (op === 'IS NULL' || op === 'IS NOT NULL') {
1035 $el.hide();
1036 return;
1037 }
1038 if (inputType === 'Date') {
1039 if (_.includes(['=', '!=', '<>', '<', '>=', '<', '<='], op)) {
1040 $el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false});
1041 }
1042 } else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op) && (field.fk_entity || field.options || dataType === 'Boolean')) {
37d82abe
CW
1043 if (field.options) {
1044 var id = field.pseudoconstant || 'id';
19b53e5b
C
1045 $el.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple: multi, data: [{id: '', text: ''}]});
1046 loadFieldOptions(field.entity || entity).then(function(data) {
37d82abe
CW
1047 var options = _.transform(data[field.name].options, function(options, opt) {
1048 options.push({id: opt[id], text: opt.label, description: opt.description, color: opt.color, icon: opt.icon});
1049 }, []);
1050 $el.removeClass('loading').crmSelect2({data: options, multiple: multi});
19b53e5b 1051 });
37d82abe
CW
1052 } else if (field.fk_entity) {
1053 $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
19b53e5b
C
1054 } else if (dataType === 'Boolean') {
1055 $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [
2929a8fb
CW
1056 {id: 'true', text: ts('Yes')},
1057 {id: 'false', text: ts('No')}
19b53e5b
C
1058 ]});
1059 }
6872a653 1060 } else if (dataType === 'Integer' && !multi) {
19b53e5b
C
1061 $el.attr('type', 'number');
1062 }
1063 }
1064
1065 function loadFieldOptions(entity) {
1066 if (!fieldOptions[entity + action]) {
1067 fieldOptions[entity + action] = crmApi4(entity, 'getFields', {
37d82abe 1068 loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
19b53e5b 1069 action: action,
37d82abe
CW
1070 where: [['options', '!=', false]],
1071 select: ['options']
1072 }, 'name');
19b53e5b
C
1073 }
1074 return fieldOptions[entity + action];
1075 }
1076
1077 // Copied from ng-list but applied conditionally if field is multi-valued
1078 var parseList = function(viewValue) {
1079 // If the viewValue is invalid (say required but empty) it will be `undefined`
1080 if (_.isUndefined(viewValue)) return;
1081
1082 if (!multi) {
1083 return viewValue;
1084 }
1085
1086 var list = [];
1087
1088 if (viewValue) {
1089 _.each(viewValue.split(','), function(value) {
1090 if (value) list.push(_.trim(value));
1091 });
1092 }
1093
1094 return list;
1095 };
1096
1097 // Copied from ng-list
1098 ctrl.$parsers.push(parseList);
1099 ctrl.$formatters.push(function(value) {
1100 return _.isArray(value) ? value.join(', ') : value;
1101 });
1102
1103 // Copied from ng-list
1104 ctrl.$isEmpty = function(value) {
1105 return !value || !value.length;
1106 };
1107
1108 scope.$watchCollection('data', function(data) {
1109 destroyWidget();
1110 var field = getField(data.field, entity, action);
af6f5ac8 1111 if (field && data.format !== 'plain') {
19b53e5b
C
1112 makeWidget(field, data.op);
1113 }
1114 });
1115 }
1116 };
1117 });
1118
1119
1120 angular.module('api4Explorer').directive('api4ExpChain', function(crmApi4) {
1121 return {
1122 scope: {
1123 chain: '=api4ExpChain',
1124 mainEntity: '=',
1125 entities: '='
1126 },
1127 templateUrl: '~/api4Explorer/Chain.html',
1128 link: function (scope, element, attrs) {
6f97b1d9 1129 var ts = scope.ts = CRM.ts();
19b53e5b
C
1130
1131 function changeEntity(newEntity, oldEntity) {
1132 // When clearing entity remove this chain
1133 if (!newEntity) {
1134 scope.chain[0] = '';
1135 return;
1136 }
1137 // Reset action && index
1138 if (newEntity !== oldEntity) {
1139 scope.chain[1][1] = scope.chain[1][2] = '';
1140 }
1141 if (getEntity(newEntity).actions) {
1142 setActions();
1143 } else {
1144 crmApi4(newEntity, 'getActions', {chain: {fields: [newEntity, 'getFields', {action: '$name'}]}})
1145 .then(function(data) {
1146 getEntity(data.entity).actions = data;
1147 if (data.entity === scope.chain[1][0]) {
1148 setActions();
1149 }
1150 });
1151 }
1152 }
1153
1154 function setActions() {
1155 scope.actions = [''].concat(_.pluck(getEntity(scope.chain[1][0]).actions, 'name'));
1156 }
1157
1158 // Set default params when choosing action
1159 function changeAction(newAction, oldAction) {
1160 var link;
1161 // Prepopulate links
1162 if (newAction && newAction !== oldAction) {
1163 // Clear index
1164 scope.chain[1][3] = '';
1165 // Look for links back to main entity
1166 _.each(entityFields(scope.chain[1][0]), function(field) {
1167 if (field.fk_entity === scope.mainEntity) {
1168 link = [field.name, '$id'];
1169 }
1170 });
1171 // Look for links from main entity
1172 if (!link && newAction !== 'create') {
1173 _.each(entityFields(scope.mainEntity), function(field) {
1174 if (field.fk_entity === scope.chain[1][0]) {
1175 link = ['id', '$' + field.name];
1176 // Since we're specifying the id, set index to getsingle
1177 scope.chain[1][3] = '0';
1178 }
1179 });
1180 }
1181 if (link && _.contains(['get', 'update', 'replace', 'delete'], newAction)) {
1182 scope.chain[1][2] = '{where: [[' + link[0] + ', =, ' + link[1] + ']]}';
1183 }
1184 else if (link && _.contains(['create'], newAction)) {
1185 scope.chain[1][2] = '{values: {' + link[0] + ': ' + link[1] + '}}';
cddf293f
CW
1186 }
1187 else if (link && _.contains(['save'], newAction)) {
1188 scope.chain[1][2] = '{records: [{' + link[0] + ': ' + link[1] + '}]}';
19b53e5b
C
1189 } else {
1190 scope.chain[1][2] = '{}';
1191 }
1192 }
1193 }
1194
1195 scope.$watch("chain[1][0]", changeEntity);
1196 scope.$watch("chain[1][1]", changeAction);
1197 }
1198 };
1199 });
1200
1201 function getEntity(entityName) {
1202 return _.findWhere(schema, {name: entityName});
1203 }
1204
1205 function entityFields(entityName, action) {
1206 var entity = getEntity(entityName);
1207 if (entity && action && entity.actions) {
1208 return _.findWhere(entity.actions, {name: action}).fields;
1209 }
1210 return _.result(entity, 'fields');
1211 }
1212
f54beb1e
CW
1213 function getExplicitJoins() {
1214 return _.transform(params.join, function(joins, join) {
1215 var j = join[0].split(' AS '),
1216 joinEntity = _.trim(j[0]),
1217 joinAlias = _.trim(j[1]) || joinEntity.toLowerCase();
1218 joins[joinAlias] = joinEntity;
1219 }, {});
1220 }
1221
19b53e5b 1222 function getField(fieldName, entity, action) {
37d82abe
CW
1223 var suffix = fieldName.split(':')[1];
1224 fieldName = fieldName.split(':')[0];
19b53e5b 1225 var fieldNames = fieldName.split('.');
37d82abe
CW
1226 var field = get(entity, fieldNames);
1227 if (field && suffix) {
1228 field.pseudoconstant = suffix;
1229 }
1230 return field;
19b53e5b
C
1231
1232 function get(entity, fieldNames) {
1233 if (fieldNames.length === 1) {
1234 return _.findWhere(entityFields(entity, action), {name: fieldNames[0]});
1235 }
1236 var comboName = _.findWhere(entityFields(entity, action), {name: fieldNames[0] + '.' + fieldNames[1]});
1237 if (comboName) {
1238 return comboName;
1239 }
1240 var linkName = fieldNames.shift(),
f54beb1e 1241 newEntity = getExplicitJoins()[linkName] || _.findWhere(links[entity], {alias: linkName}).entity;
19b53e5b
C
1242 return get(newEntity, fieldNames);
1243 }
1244 }
1245
1246 // Collapsible optgroups for select2
1247 $(function() {
1248 $('body')
1249 .on('select2-open', function(e) {
1250 if ($(e.target).hasClass('collapsible-optgroups')) {
1251 $('#select2-drop')
1252 .off('.collapseOptionGroup')
1253 .addClass('collapsible-optgroups-enabled')
1254 .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() {
1255 $(this).parent().toggleClass('optgroup-expanded');
1256 });
1257 }
1258 })
1259 .on('select2-close', function() {
1260 $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled');
1261 });
1262 });
1263})(angular, CRM.$, CRM._);