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