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