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