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