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