$scope.status = 'default';
$scope.loading = false;
$scope.controls = {};
- $scope.code = [
- {
- lang: 'php',
- style: [
- {name: 'oop', label: ts('OOP Style'), code: ''},
- {name: 'php', label: ts('Traditional'), code: ''}
- ]
- },
- {
- lang: 'js',
- style: [
- {name: 'js', label: ts('Single Call'), code: ''},
- {name: 'js2', label: ts('Batch Calls'), code: ''}
- ]
- },
- {
- lang: 'ang',
- style: [
- {name: 'ang', label: ts('Single Call'), code: ''},
- {name: 'ang2', label: ts('Batch Calls'), code: ''}
- ]
- },
- {
- lang: 'cli',
- style: [
- {name: 'cv', label: ts('CV'), code: ''}
- ]
- },
- ];
+ $scope.langs = ['php', 'js', 'ang', 'cli'];
+ $scope.joinTypes = [{k: false, v: ts('Optional')}, {k: true, v: ts('Required')}];
+ $scope.code = {
+ php: [
+ {name: 'oop', label: ts('OOP Style'), code: ''},
+ {name: 'php', label: ts('Traditional'), code: ''}
+ ],
+ js: [
+ {name: 'js', label: ts('Single Call'), code: ''},
+ {name: 'js2', label: ts('Batch Calls'), code: ''}
+ ],
+ ang: [
+ {name: 'ang', label: ts('Single Call'), code: ''},
+ {name: 'ang2', label: ts('Batch Calls'), code: ''}
+ ],
+ cli: [
+ {name: 'cv', label: ts('CV'), code: ''}
+ ]
+ };
if (!entities.length) {
formatForSelect2(schema, entities, 'name', ['description']);
return container;
}
- function getFieldList(action) {
+ // Returns field list formatted for select2
+ function getFieldList(action, addPseudoconstant) {
var fields = [],
fieldInfo = _.findWhere(getEntity().actions, {name: action}).fields;
+ if (addPseudoconstant) {
+ fieldInfo = _.cloneDeep(fieldInfo);
+ addPseudoconstants(fieldInfo, addPseudoconstant);
+ }
formatForSelect2(fieldInfo, fields, 'name', ['description', 'required', 'default_value']);
return fields;
}
- function addJoins(fieldList, addWildcard) {
- var fields = _.cloneDeep(fieldList),
- fks = _.findWhere(links, {entity: $scope.entity}) || {};
- _.each(fks.links, function(link) {
+ // Note: this function expects fieldList to be select2-formatted already
+ function addJoins(fieldList, addWildcard, addPseudoconstant) {
+ var fields = _.cloneDeep(fieldList);
+ _.each(links[$scope.entity], function(link) {
var linkFields = _.cloneDeep(entityFields(link.entity)),
wildCard = addWildcard ? [{id: link.alias + '.*', text: link.alias + '.*', 'description': 'All core ' + link.entity + ' fields'}] : [];
if (linkFields) {
+ if (addPseudoconstant) {
+ addPseudoconstants(linkFields, addPseudoconstant);
+ }
fields.push({
text: link.alias,
- description: 'Join to ' + link.entity,
+ description: 'Implicit join to ' + link.entity,
children: wildCard.concat(formatForSelect2(linkFields, [], 'name', ['description'], link.alias + '.'))
});
}
return fields;
}
+ // Note: this function transforms a raw list a-la getFields; not a select2-formatted list
+ function addPseudoconstants(fieldList, toAdd) {
+ var optionFields = _.filter(fieldList, 'options');
+ _.each(optionFields, function(field) {
+ var pos = _.findIndex(fieldList, {name: field.name}) + 1;
+ _.each(toAdd, function(suffix) {
+ var newField = _.cloneDeep(field);
+ newField.name += ':' + suffix;
+ fieldList.splice(pos, 0, newField);
+ });
+ });
+ }
+
$scope.help = function(title, content) {
if (!content) {
$scope.helpTitle = helpTitle;
return info;
};
+ // Returns field list for write params (values, defaults)
$scope.fieldList = function(param) {
return function() {
- var fields = _.cloneDeep($scope.action === 'getFields' ? getFieldList($scope.params.action || 'get') : $scope.fields);
+ var fields = _.cloneDeep(getFieldList($scope.action === 'getFields' ? ($scope.params.action || 'get') : $scope.action, ['name']));
// Disable fields that are already in use
_.each($scope.params[param] || [], function(val) {
- (_.findWhere(fields, {id: val[0]}) || {}).disabled = true;
+ var usedField = val[0].replace(':name', '');
+ (_.findWhere(fields, {id: usedField}) || {}).disabled = true;
+ (_.findWhere(fields, {id: usedField + ':name'}) || {}).disabled = true;
});
return {results: fields};
};
}
};
- $scope.isSpecial = function(name) {
- var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy', 'having'];
+ // Gets params that should be represented as generic input fields in the explorer
+ // This fn doesn't have to be particularly efficient as its output is cached in one-time bindings
+ $scope.getGenericParams = function(paramType, defaultNull) {
+ // Returns undefined if params are not yet set; one-time bindings will stabilize when this function returns a value
+ if (_.isEmpty($scope.availableParams)) {
+ return;
+ }
+ var specialParams = ['select', 'fields', 'action', 'where', 'values', 'defaults', 'orderBy', 'chain', 'groupBy', 'having', 'join'];
if ($scope.availableParams.limit && $scope.availableParams.offset) {
specialParams.push('limit', 'offset');
}
- return _.contains(specialParams, name);
+ return _.transform($scope.availableParams, function(genericParams, param, name) {
+ if (!_.contains(specialParams, name) &&
+ !(typeof paramType !== 'undefined' && !_.contains(paramType, param.type[0])) &&
+ !(typeof defaultNull !== 'undefined' && ((param.default === null) !== defaultNull))
+ ) {
+ genericParams[name] = param;
+ }
+ });
};
$scope.selectRowCount = function() {
return isSelectRowCount($scope.params);
};
+ $scope.selectLang = function(lang) {
+ $scope.selectedTab.code = lang;
+ writeCode();
+ };
+
function isSelectRowCount(params) {
return params && params.select && params.select.length === 1 && params.select[0] === 'row_count';
}
var actionInfo = _.findWhere(actions, {id: $scope.action});
$scope.fields = getFieldList($scope.action);
if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) {
- $scope.fieldsAndJoins = addJoins($scope.fields);
- var fieldsAndFunctions = _.cloneDeep($scope.fields);
+ $scope.fieldsAndJoins = addJoins(getFieldList($scope.action, ['name']));
+ var functions = [];
// SQL functions are supported if HAVING is
if (actionInfo.params.having) {
- fieldsAndFunctions.push({
+ functions.push({
text: ts('FUNCTION'),
description: ts('Calculate result of a SQL function'),
children: _.transform(CRM.vars.api4.functions, function(result, fn) {
})
});
}
- $scope.fieldsAndJoinsAndFunctions = addJoins(fieldsAndFunctions, true);
- $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(fieldsAndFunctions, true);
+ $scope.fieldsAndJoinsAndFunctions = addJoins($scope.fields.concat(functions), true);
+ $scope.fieldsAndJoinsAndFunctionsAndWildcards = addJoins(getFieldList($scope.action, ['name', 'label']).concat(functions), true, ['name', 'label']);
} else {
- $scope.fieldsAndJoins = $scope.fields;
+ $scope.fieldsAndJoins = getFieldList($scope.action, ['name']);
$scope.fieldsAndJoinsAndFunctions = $scope.fields;
- $scope.fieldsAndJoinsAndFunctionsAndWildcards = _.cloneDeep($scope.fields);
+ $scope.fieldsAndJoinsAndFunctionsAndWildcards = getFieldList($scope.action, ['name', 'label']);
}
$scope.fieldsAndJoinsAndFunctionsAndWildcards.unshift({id: '*', text: '*', 'description': 'All core ' + $scope.entity + ' fields'});
_.each(actionInfo.params, function (param, name) {
if (name === 'values') {
defaultVal = defaultValues(defaultVal);
}
+ if (name === 'loadOptions' && $scope.action === 'getFields') {
+ param.options = [
+ false,
+ true,
+ ['id', 'name', 'label'],
+ ['id', 'name', 'label', 'abbr', 'description', 'color', 'icon']
+ ];
+ format = 'json';
+ defaultVal = false;
+ param.type = ['string'];
+ }
$scope.$bindToRoute({
expr: 'params["' + name + '"]',
param: name,
$scope.havingOptions.length = 0;
_.each(values, function(item) {
var pieces = item.split(' AS '),
- alias = _.trim(pieces[pieces.length - 1]);
+ alias = _.trim(pieces[pieces.length - 1]).replace(':label', ':name');
$scope.havingOptions.push({id: alias, text: alias});
});
});
}
- if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select') {
+ if (typeof objectParams[name] !== 'undefined' || name === 'groupBy' || name === 'select' || name === 'join') {
$scope.$watch('controls.' + name, function(value) {
var field = value;
$timeout(function() {
if (field) {
- if (typeof objectParams[name] === 'undefined') {
+ if (name === 'join') {
+ $scope.params[name].push([field + ' AS ' + _.snakeCase(field), false, '[]']);
+ }
+ else if (typeof objectParams[name] === 'undefined') {
$scope.params[name].push(field);
} else {
var defaultOp = _.cloneDeep(objectParams[name]);
results = result + 'Count';
}
- // Write javascript
- var js = "'" + entity + "', '" + action + "', {";
- _.each(params, function(param, key) {
- js += "\n " + key + ': ' + stringify(param) +
- (++i < paramCount ? ',' : '');
- if (key === 'checkPermissions') {
- js += ' // IGNORED: permissions are always enforced from client-side requests';
- }
- });
- js += "\n}";
- if (index || index === 0) {
- js += ', ' + JSON.stringify(index);
- }
- code.js = "CRM.api4(" + js + ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
- code.js2 = "CRM.api4({" + results + ': [' + js + "]}).then(function(batch) {\n // do something with batch." + results + " array\n}, function(failure) {\n // handle failure\n});";
- code.ang = "crmApi4(" + js + ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
- code.ang2 = "crmApi4({" + results + ': [' + js + "]}).then(function(batch) {\n // do something with batch." + results + " array\n}, function(failure) {\n // handle failure\n});";
-
- // Write php code
- code.php = '$' + results + " = civicrm_api4('" + entity + "', '" + action + "', [";
- _.each(params, function(param, key) {
- code.php += "\n '" + key + "' => " + phpFormat(param, 4) + ',';
- });
- code.php += "\n]";
- if (index || index === 0) {
- code.php += ', ' + phpFormat(index);
- }
- code.php += ");";
+ switch ($scope.selectedTab.code) {
+ case 'js':
+ case 'ang':
+ // Write javascript
+ var js = "'" + entity + "', '" + action + "', {";
+ _.each(params, function(param, key) {
+ js += "\n " + key + ': ' + stringify(param) +
+ (++i < paramCount ? ',' : '');
+ if (key === 'checkPermissions') {
+ js += ' // IGNORED: permissions are always enforced from client-side requests';
+ }
+ });
+ js += "\n}";
+ if (index || index === 0) {
+ js += ', ' + JSON.stringify(index);
+ }
+ code.js = "CRM.api4(" + js + ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
+ code.js2 = "CRM.api4({" + results + ': [' + js + "]}).then(function(batch) {\n // do something with batch." + results + " array\n}, function(failure) {\n // handle failure\n});";
+ code.ang = "crmApi4(" + js + ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});";
+ code.ang2 = "crmApi4({" + results + ': [' + js + "]}).then(function(batch) {\n // do something with batch." + results + " array\n}, function(failure) {\n // handle failure\n});";
+ break;
+
+ case 'php':
+ // Write php code
+ code.php = '$' + results + " = civicrm_api4('" + entity + "', '" + action + "', [";
+ _.each(params, function(param, key) {
+ code.php += "\n '" + key + "' => " + phpFormat(param, 4) + ',';
+ });
+ code.php += "\n]";
+ if (index || index === 0) {
+ code.php += ', ' + phpFormat(index);
+ }
+ code.php += ");";
+
+ // Write oop code
+ code.oop = '$' + results + " = " + formatOOP(entity, action, params, 2) + "\n ->execute()";
+ if (isSelectRowCount(params)) {
+ code.oop += "\n ->count()";
+ } else if (_.isNumber(index)) {
+ code.oop += !index ? '\n ->first()' : (index === -1 ? '\n ->last()' : '\n ->itemAt(' + index + ')');
+ } else if (index) {
+ if (_.isString(index) || (_.isPlainObject(index) && !index[0] && !index['0'])) {
+ code.oop += "\n ->indexBy('" + (_.isPlainObject(index) ? _.keys(index)[0] : index) + "')";
+ }
+ if (_.isArray(index) || _.isPlainObject(index)) {
+ code.oop += "\n ->column('" + (_.isArray(index) ? index[0] : _.values(index)[0]) + "')";
+ }
+ }
+ code.oop += ";\n";
+ if (!_.isNumber(index) && !isSelectRowCount(params)) {
+ code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n // do something\n}';
+ }
+ break;
- // Write oop code
- code.oop = '$' + results + " = " + formatOOP(entity, action, params, 2) + "\n ->execute()";
- if (isSelectRowCount(params)) {
- code.oop += "\n ->count()";
- } else if (_.isNumber(index)) {
- code.oop += !index ? '\n ->first()' : (index === -1 ? '\n ->last()' : '\n ->itemAt(' + index + ')');
- } else if (index) {
- if (_.isString(index) || (_.isPlainObject(index) && !index[0] && !index['0'])) {
- code.oop += "\n ->indexBy('" + (_.isPlainObject(index) ? _.keys(index)[0] : index) + "')";
- }
- if (_.isArray(index) || _.isPlainObject(index)) {
- code.oop += "\n ->column('" + (_.isArray(index) ? index[0] : _.values(index)[0]) + "')";
- }
- }
- code.oop += ";\n";
- if (!_.isNumber(index) && !isSelectRowCount(params)) {
- code.oop += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n // do something\n}';
+ case 'cli':
+ // Write cli code
+ code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
}
-
- // Write cli code
- code.cv = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'";
}
_.each($scope.code, function(vals) {
- _.each(vals.style, function(style) {
+ _.each(vals, function(style) {
style.code = code[style.name] ? prettyPrintOne(code[style.name]) : '';
});
});
}
$scope.execute = function() {
- $scope.status = 'warning';
+ $scope.status = 'info';
$scope.loading = true;
$http.post(CRM.url('civicrm/ajax/api4/' + $scope.entity + '/' + $scope.action, {
params: angular.toJson(getParams()),
}
}).then(function(resp) {
$scope.loading = false;
- $scope.status = 'success';
+ $scope.status = resp.data && resp.data.debug && resp.data.debug.log ? 'warning' : 'success';
$scope.debug = debugFormat(resp.data);
$scope.result = [formatMeta(resp.data), prettyPrintOne(_.escape(JSON.stringify(resp.data.values, null, 2)), 'js', 1)];
}, function(resp) {
$scope.saveDoc = function() {
return {
description: ts('Save API call as a smart group.'),
- comment: ts('Allows you to create a SavedSearch containing the WHERE clause of this API call.'),
+ comment: ts('Create a SavedSearch using these API params to populate a smart group.') +
+ '\n\n' + ts('NOTE: you must select contact id as the only field.')
};
};
writeCode();
$scope.save = function() {
+ $scope.params.limit = $scope.params.offset = 0;
+ if ($scope.params.chain.length) {
+ CRM.alert(ts('Smart groups are not compatible with API chaining.'), ts('Error'), 'error', {expires: 5000});
+ return;
+ }
+ if ($scope.params.select.length !== 1 || !_.includes($scope.params.select[0], 'id')) {
+ CRM.alert(ts('To create a smart group, the API must select contact id and no other fields.'), ts('Error'), 'error', {expires: 5000});
+ return;
+ }
var model = {
title: '',
description: '',
params: JSON.parse(angular.toJson($scope.params))
};
model.params.version = 4;
- delete model.params.select;
delete model.params.chain;
delete model.params.debug;
delete model.params.limit;
+ delete model.params.offset;
+ delete model.params.orderBy;
delete model.params.checkPermissions;
var options = CRM.utils.adjustDialogDefaults({
width: '500px',
$el.crmDatepicker({time: (field.input_attrs && field.input_attrs.time) || false});
}
} else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op) && (field.fk_entity || field.options || dataType === 'Boolean')) {
- if (field.fk_entity) {
- $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
- } else if (field.options) {
+ if (field.options) {
+ var id = field.pseudoconstant || 'id';
$el.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple: multi, data: [{id: '', text: ''}]});
loadFieldOptions(field.entity || entity).then(function(data) {
- var options = [];
- _.each(_.findWhere(data, {name: field.name}).options, function(val, key) {
- options.push({id: key, text: val});
- });
- $el.removeClass('loading').select2({data: options, multiple: multi});
+ var options = _.transform(data[field.name].options, function(options, opt) {
+ options.push({id: opt[id], text: opt.label, description: opt.description, color: opt.color, icon: opt.icon});
+ }, []);
+ $el.removeClass('loading').crmSelect2({data: options, multiple: multi});
});
+ } else if (field.fk_entity) {
+ $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}});
} else if (dataType === 'Boolean') {
$el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [
{id: 'true', text: ts('Yes')},
function loadFieldOptions(entity) {
if (!fieldOptions[entity + action]) {
fieldOptions[entity + action] = crmApi4(entity, 'getFields', {
- loadOptions: true,
+ loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'],
action: action,
- where: [["options", "!=", false]],
- select: ["name", "options"]
- });
+ where: [['options', '!=', false]],
+ select: ['options']
+ }, 'name');
}
return fieldOptions[entity + action];
}
}
function getField(fieldName, entity, action) {
+ var suffix = fieldName.split(':')[1];
+ fieldName = fieldName.split(':')[0];
var fieldNames = fieldName.split('.');
- return get(entity, fieldNames);
+ var field = get(entity, fieldNames);
+ if (field && suffix) {
+ field.pseudoconstant = suffix;
+ }
+ return field;
function get(entity, fieldNames) {
if (fieldNames.length === 1) {
return comboName;
}
var linkName = fieldNames.shift(),
- entityLinks = _.findWhere(links, {entity: entity}).links,
- newEntity = _.findWhere(entityLinks, {alias: linkName}).entity;
+ newEntity = _.findWhere(links[entity], {alias: linkName}).entity;
return get(newEntity, fieldNames);
}
}