diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang')
9 files changed, 1044 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.ang.php new file mode 100644 index 00000000..f7d69b39 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.ang.php @@ -0,0 +1,12 @@ +<?php +// Autoloader data for Api4 angular module. +return [ + 'js' => [ + 'ang/api4.js', + 'ang/api4/*.js', + 'ang/api4/*/*.js', + ], + 'css' => [], + 'partials' => [], + 'requires' => [], +]; diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.js b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.js new file mode 100644 index 00000000..d1116fc4 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.js @@ -0,0 +1,4 @@ +(function(angular, $, _) { + // Declare a list of dependencies. + angular.module('api4', CRM.angRequires('api4')); +})(angular, CRM.$, CRM._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4/crmApi4.js b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4/crmApi4.js new file mode 100644 index 00000000..743b3591 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4/crmApi4.js @@ -0,0 +1,37 @@ +(function(angular, $, _) { + + angular.module('api4').factory('crmApi4', function($q) { + var crmApi4 = function(entity, action, params, index) { + // JSON serialization in CRM.api4 is not aware of Angular metadata like $$hash, so use angular.toJson() + var deferred = $q.defer(); + var p; + var backend = crmApi4.backend || CRM.api4; + if (_.isObject(entity)) { + // eval content is locally generated. + /*jshint -W061 */ + p = backend(eval('('+angular.toJson(entity)+')'), action); + } else { + // eval content is locally generated. + /*jshint -W061 */ + p = backend(entity, action, eval('('+angular.toJson(params)+')'), index); + } + p.then( + function(result) { + deferred.resolve(result); + }, + function(error) { + deferred.reject(error); + } + ); + return deferred.promise; + }; + crmApi4.backend = null; + crmApi4.val = function(value) { + var d = $.Deferred(); + d.resolve(value); + return d.promise(); + }; + return crmApi4; + }); + +})(angular, CRM.$, CRM._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.ang.php b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.ang.php new file mode 100644 index 00000000..6583f277 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.ang.php @@ -0,0 +1,18 @@ +<?php +// Autoloader data for Api4 explorer. +return [ + 'js' => [ + 'ang/api4Explorer.js', + 'ang/api4Explorer/*.js', + 'ang/api4Explorer/*/*.js', + 'lib/*.js', + ], + 'css' => [ + 'css/explorer.css', + ], + 'partials' => [ + 'ang/api4Explorer', + ], + 'basePages' => [], + 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'crmRouteBinder', 'ui.sortable', 'api4'], +]; diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.js b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.js new file mode 100644 index 00000000..85e10c46 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.js @@ -0,0 +1,4 @@ +(function(angular, $, _) { + // Declare a list of dependencies. + angular.module('api4Explorer', CRM.angRequires('api4Explorer')); +})(angular, CRM.$, CRM._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Chain.html b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Chain.html new file mode 100644 index 00000000..257efdec --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Chain.html @@ -0,0 +1,4 @@ +<input class="form-control" ng-model="chain[1][0]" crm-ui-select="{data: entities, allowClear: true, placeholder: 'None'}" /> +<select class="form-control api4-chain-action" ng-model="chain[1][1]" ng-options="a for a in actions" ></select> +<input class="form-control api4-chain-params" ng-model="chain[1][2]" placeholder="{{ ts('Params') }}" /> +<input class="form-control api4-chain-index" ng-model="chain[1][3]" placeholder="{{ ts('Index') }}" /> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.html b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.html new file mode 100644 index 00000000..6cea301f --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.html @@ -0,0 +1,137 @@ +<div id="bootstrap-theme" class="api4-explorer-page"> + <div crm-ui-debug="availableParams"></div> + + <h1 crm-page-title> + {{ ts('CiviCRM API v4') }}{{ entity ? (' (' + entity + '::' + action + ')') : '' }} + </h1> + + <!--This warning will show if bootstrap is unavailable. Normally it will be hidden by the bootstrap .collapse class.--> + <div class="messages warning no-popup collapse"> + <p> + <i class="crm-i fa-exclamation-triangle"></i> + <strong>{{ ts('Bootstrap theme not found.') }}</strong> + </p> + <p>{{ ts('This screen may not work correctly without a bootstrap-based theme such as Shoreditch installed.') }}</p> + </div> + + <div class="api4-explorer-row"> + <form name="api4-explorer" class="panel panel-default explorer-params-panel"> + <div class="panel-heading"> + <div class="form-inline"> + <input class="collapsible-optgroups form-control" ng-model="entity" ng-disabled="!entities.length" ng-class="{loading: !entities.length}" crm-ui-select="{placeholder: ts('Entity'), data: entities}" /> + <input class="collapsible-optgroups form-control" ng-model="action" ng-disabled="!entity || !actions.length" ng-class="{loading: entity && !actions.length}" crm-ui-select="{placeholder: ts('Action'), data: actions}" /> + <input class="form-control api4-index" ng-model="index" ng-mouseenter="help('index', indexHelp)" ng-mouseleave="help()" placeholder="{{ ts('Index') }}" /> + <button class="btn btn-success pull-right" crm-icon="fa-bolt" ng-disabled="!entity || !action || loading" ng-click="execute()">{{ ts('Execute') }}</button> + </div> + </div> + <div class="panel-body"> + <div class="api4-input form-inline"> + <div class="form-control" ng-mouseenter="help(name, param)" ng-mouseleave="help()" ng-class="{'api4-option-selected': params[name]}" ng-repeat="(name, param) in availableParams" ng-if="!isSpecial(name) && param.type[0] === 'bool'"> + <input type="checkbox" id="api4-param-{{ name }}" ng-model="params[name]"/> + <label for="api4-param-{{ name }}">{{ name }}<span class="crm-marker" ng-if="param.required"> *</span></label> + </div> + </div> + <div class="api4-input form-inline" ng-mouseenter="help('select', availableParams.select)" ng-mouseleave="help()" ng-if="availableParams.select"> + <label for="api4-param-select">select<span class="crm-marker" ng-if="availableParams.select.required"> *</span></label> + <input class="collapsible-optgroups form-control" ng-list crm-ui-select="{data: fieldsAndJoins, multiple: true}" id="api4-param-select" ng-model="params.select" style="width: 85%;"/> + </div> + <div class="api4-input form-inline" ng-mouseenter="help('fields', availableParams.fields)" ng-mouseleave="help()"ng-if="availableParams.fields"> + <label for="api4-param-fields">fields<span class="crm-marker" ng-if="availableParams.fields.required"> *</span></label> + <input class="form-control" ng-list crm-ui-select="{data: fields, multiple: true}" id="api4-param-fields" ng-model="params.fields" style="width: 85%;"/> + </div> + <div class="api4-input form-inline" ng-mouseenter="help('action', availableParams.action)" ng-mouseleave="help()"ng-if="availableParams.action"> + <label for="api4-param-action">action<span class="crm-marker" ng-if="availableParams.action.required"> *</span></label> + <input class="form-control" crm-ui-select="{data: actions, allowClear: true, placeholder: 'None'}" id="api4-param-action" ng-model="params.action"/> + </div> + <div class="api4-input form-inline" ng-mouseenter="help(name, param)" ng-mouseleave="help()" ng-repeat="(name, param) in availableParams" ng-if="!isSpecial(name) && (param.type[0] === 'string' || param.type[0] === 'int')"> + <label for="api4-param-{{ name }}">{{ name }}<span class="crm-marker" ng-if="param.required"> *</span></label> + <input class="form-control" type="{{ param.type[0] === 'int' && param.type.length === 1 ? 'number' : 'text' }}" id="api4-param-{{ name }}" ng-model="params[name]"/> + <a href class="crm-hover-button" title="Clear" ng-click="clearParam(name)" ng-show="!!params[name]"><i class="crm-i fa-times"></i></a> + </div> + <div class="api4-input" ng-mouseenter="help(name, param)" ng-mouseleave="help()" ng-repeat="(name, param) in availableParams" ng-if="!isSpecial(name) && param.type[0] === 'array'"> + <label for="api4-param-{{ name }}">{{ name }}<span class="crm-marker" ng-if="param.required"> *</span></label> + <textarea class="form-control" type="{{ param.type[0] === 'int' && param.type.length === 1 ? 'number' : 'text' }}" id="api4-param-{{ name }}" ng-model="params[name]"> + </textarea> + </div> + <fieldset ng-if="availableParams.where" class="api4-where-fieldset" ng-mouseenter="help('where', availableParams.where)" ng-mouseleave="help()" crm-api4-where-clause="{where: params.where, required: availableParams.where.required, op: 'AND', label: 'where', fields: fieldsAndJoins}"> + </fieldset> + <fieldset ng-if="availableParams.values" ng-mouseenter="help('values', availableParams.values)" ng-mouseleave="help()"> + <legend>values<span class="crm-marker" ng-if="availableParams.values.required"> *</span></legend> + <div class="api4-input form-inline" ng-repeat="clause in params.values"> + <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{formatResult: formatSelect2Item, formatSelection: formatSelect2Item, data: valuesFields, allowClear: true, placeholder: 'Field'}" /> + <input class="form-control" ng-model="clause[1]" api4-exp-value="{field: clause[0]}" /> + </div> + <div class="api4-input form-inline"> + <input class="collapsible-optgroups form-control" ng-model="controls.values" crm-ui-select="{formatResult: formatSelect2Item, formatSelection: formatSelect2Item, data: valuesFields}" placeholder="Add value" /> + </div> + </fieldset> + <fieldset ng-if="availableParams.orderBy" ng-mouseenter="help('orderBy', availableParams.orderBy)" ng-mouseleave="help()"> + <legend>orderBy<span class="crm-marker" ng-if="availableParams.orderBy.required"> *</span></legend> + <div class="api4-input form-inline" ng-repeat="clause in params.orderBy"> + <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: fieldsAndJoins, allowClear: true, placeholder: 'Field'}" /> + <select class="form-control" ng-model="clause[1]"> + <option value="ASC">ASC</option> + <option value="DESC">DESC</option> + </select> + </div> + <div class="api4-input form-inline"> + <input class="collapsible-optgroups form-control" ng-model="controls.orderBy" crm-ui-select="{data: fieldsAndJoins}" placeholder="Add orderBy" /> + </div> + </fieldset> + <fieldset ng-if="availableParams.chain" ng-mouseenter="help('chain', availableParams.chain)" ng-mouseleave="help()"> + <legend>chain</legend> + <div class="api4-input form-inline" ng-repeat="clause in params.chain" api4-exp-chain="clause" entities="entities" main-entity="entity" > + </div> + <div class="api4-input form-inline"> + <input class="form-control" ng-model="controls.chain" crm-ui-select="{data: entities}" placeholder="Add chain" /> + </div> + </fieldset> + </div> + </form> + <div class="panel panel-info explorer-help-panel"> + <div class="panel-heading"> + <h3 class="panel-title" crm-icon="fa-info-circle">{{ helpTitle }}</h3> + </div> + <div class="panel-body"> + <h4>{{ helpContent.description }}</h4> + <div ng-if="helpContent.comment"> + <p ng-repeat='text in helpContent.comment.split("\n\n")'>{{ text }}</p> + </div> + <p ng-repeat="(key, item) in helpContent" ng-if="key !== 'description' && key !== 'comment'"> + <strong>{{ key }}:</strong> {{ item }} + </p> + </div> + </div> + </div> + <div class="api4-explorer-row"> + <div class="panel panel-warning explorer-code-panel"> + <div class="panel-heading"> + <h3 class="panel-title" crm-icon="fa-code">{{ ts('Code') }}</h3> + </div> + <div class="panel-body"> + <table> + <tr ng-repeat="(type, item) in code"> + <td>{{ type }}</td> + <td><pre>{{ item }}</pre></td> + </tr> + </table> + </div> + </div> + <div class="panel explorer-result-panel panel-{{ status }}" > + <div class="panel-heading"> + <h3 class="panel-title"> + <i class="fa fa-circle-o" ng-if="status === 'default'"></i> + <i class="fa fa-check-circle" ng-if="status === 'success'"></i> + <i class="fa fa-minus-circle" ng-if="status === 'danger'"></i> + <i class="fa fa-spinner fa-pulse" ng-if="status === 'warning'"></i> + {{ ts('Result') }} + </h3> + </div> + <div class="panel-body"> + <pre ng-repeat="code in result">{{ code }}</pre> + </div> + </div> + </div> + + +</div> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.js b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.js new file mode 100644 index 00000000..10391793 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.js @@ -0,0 +1,789 @@ +(function(angular, $, _, undefined) { + + // Cache schema metadata + var schema = []; + // Cache fk schema data + var links = []; + // Cache list of entities + var entities = []; + // Cache list of actions + var actions = []; + // Field options + var fieldOptions = {}; + + + angular.module('api4Explorer').config(function($routeProvider) { + $routeProvider.when('/explorer/:api4entity?/:api4action?', { + controller: 'Api4Explorer', + templateUrl: '~/api4Explorer/Explorer.html', + reloadOnSearch: false + }); + }); + + angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, crmUiHelp, crmApi4) { + var ts = $scope.ts = CRM.ts('api4'); + $scope.entities = entities; + $scope.actions = actions; + $scope.fields = []; + $scope.fieldsAndJoins = []; + $scope.availableParams = {}; + $scope.params = {}; + $scope.index = ''; + var getMetaParams = schema.length ? {} : { + schema: ['Entity', 'get', {chain: {fields: ['$name', 'getFields']}}], + links: ['Entity', 'getLinks'] + }, + objectParams = {orderBy: 'ASC', values: '', chain: ['Entity', '', '{}']}, + helpTitle = '', + helpContent = {}; + $scope.helpTitle = ''; + $scope.helpContent = {}; + $scope.entity = $routeParams.api4entity; + $scope.result = []; + $scope.status = 'default'; + $scope.loading = false; + $scope.controls = {}; + $scope.code = { + php: '', + javascript: '', + cli: '' + }; + + $scope.$bindToRoute({ + expr: 'index', + param: 'index', + default: '' + }); + + function ucfirst(str) { + return str[0].toUpperCase() + str.slice(1); + } + + function lcfirst(str) { + return str[0].toLowerCase() + str.slice(1); + } + + function pluralize(str) { + switch (str[str.length-1]) { + case 's': + return str + 'es'; + case 'y': + return str.slice(0, -1) + 'ies'; + default: + return str + 's'; + } + } + + // Turn a flat array into a select2 array + function arrayToSelect2(array) { + var out = []; + _.each(array, function(item) { + out.push({id: item, text: item}); + }); + return out; + } + + // Reformat an existing array of objects for compatibility with select2 + function formatForSelect2(input, container, key, extra, prefix) { + _.each(input, function(item) { + var id = (prefix || '') + item[key]; + var formatted = {id: id, text: id}; + if (extra) { + _.merge(formatted, _.pick(item, extra)); + } + container.push(formatted); + }); + return container; + } + + function getFieldList(source) { + var fields = [], + fieldInfo = _.findWhere(getEntity().actions, {name: $scope.action}).fields; + formatForSelect2(fieldInfo, fields, 'name', ['description', 'required', 'default_value']); + return fields; + } + + function addJoins(fieldList) { + var fields = _.cloneDeep(fieldList), + fks = _.findWhere(links, {entity: $scope.entity}) || {}; + _.each(fks.links, function(link) { + var linkFields = entityFields(link.entity); + if (linkFields) { + fields.push({ + text: link.alias, + description: 'Join to ' + link.entity, + children: formatForSelect2(linkFields, [], 'name', ['description'], link.alias + '.') + }); + } + }); + return fields; + } + + $scope.help = function(title, param) { + if (!param) { + $scope.helpTitle = helpTitle; + $scope.helpContent = helpContent; + } else { + $scope.helpTitle = title; + $scope.helpContent = param; + } + }; + + $scope.valuesFields = function() { + var fields = _.cloneDeep($scope.fields); + // Disable fields that are already in use + _.each($scope.params.values || [], function(val) { + (_.findWhere(fields, {id: val[0]}) || {}).disabled = true; + }); + return {results: fields}; + }; + + $scope.formatSelect2Item = function(row) { + return _.escape(row.text) + + (row.required ? '<span class="crm-marker"> *</span>' : '') + + (row.description ? '<div class="crm-select2-row-description"><p>' + _.escape(row.description) + '</p></div>' : ''); + }; + + $scope.clearParam = function(name) { + $scope.params[name] = $scope.availableParams[name].default; + }; + + $scope.isSpecial = function(name) { + var specialParams = ['select', 'fields', 'action', 'where', 'values', 'orderBy', 'chain']; + return _.contains(specialParams, name); + }; + + function getEntity(entityName) { + return _.findWhere(schema, {name: entityName || $scope.entity}); + } + + // Get all params that have been set + function getParams() { + var params = {}; + _.each($scope.params, function(param, key) { + if (param != $scope.availableParams[key].default && !(typeof param === 'object' && _.isEmpty(param))) { + if (_.contains($scope.availableParams[key].type, 'array') && (typeof objectParams[key] === 'undefined')) { + params[key] = parseYaml(_.cloneDeep(param)); + } else { + params[key] = param; + } + } + }); + _.each(objectParams, function(defaultVal, key) { + if (params[key]) { + var newParam = {}; + _.each(params[key], function(item) { + newParam[item[0]] = parseYaml(_.cloneDeep(item[1])); + }); + params[key] = newParam; + } + }); + if (params.where) { + formatWhereClause(params.where); + } + return params; + } + + // Coerce value to an array when the operator is IN or NOT IN + // Note this has already been passed through parseYaml once + function formatWhereClause(where) { + _.each(where, function(clause) { + if (_.isArray(clause)) { + if (clause.length === 3) { + if (_.contains(['IN', 'NOT IN'], clause[1]) && (_.isNumber(clause[2]) || (_.isString(clause[2]) && clause[2].length))) { + clause[2] = parseYaml('[' + clause[2] + ']'); + } + } else { + formatWhereClause(clause); + } + } + }); + } + + function parseYaml(input) { + if (typeof input === 'undefined') { + return undefined; + } + if (_.isObject(input) || _.isArray(input)) { + _.each(input, function(item, index) { + input[index] = parseYaml(item); + }); + return input; + } + try { + return input === '>' ? '>' : jsyaml.safeLoad(input); + } catch (e) { + return input; + } + } + + function selectAction() { + $scope.action = $routeParams.api4action; + $scope.fieldsAndJoins = []; + formatForSelect2(getEntity().actions, actions, 'name', ['description', 'params']); + if ($scope.action) { + var actionInfo = _.findWhere(actions, {id: $scope.action}); + $scope.fields = getFieldList(); + if (_.contains(['get', 'update', 'delete', 'replace'], $scope.action)) { + $scope.fieldsAndJoins = addJoins($scope.fields); + } else { + $scope.fieldsAndJoins = $scope.fields; + } + _.each(actionInfo.params, function (param, name) { + var format, + defaultVal = _.cloneDeep(param.default); + if (param.type) { + switch (param.type[0]) { + case 'int': + case 'bool': + format = param.type[0]; + break; + + case 'array': + case 'object': + format = 'json'; + break; + + default: + format = 'raw'; + } + if (name == 'limit') { + defaultVal = 25; + } + if (name === 'values') { + defaultVal = defaultValues(defaultVal); + } + $scope.$bindToRoute({ + expr: 'params["' + name + '"]', + param: name, + format: format, + default: defaultVal, + deep: format === 'json' + }); + } + if (typeof objectParams[name] !== 'undefined') { + $scope.$watch('params.' + name, function(values) { + // Remove empty values + _.each(values, function(clause, index) { + if (!clause || !clause[0]) { + $scope.params[name].splice(index, 1); + } + }); + }, true); + $scope.$watch('controls.' + name, function(value) { + var field = value; + $timeout(function() { + if (field) { + var defaultOp = _.cloneDeep(objectParams[name]); + if (name === 'chain') { + var num = $scope.params.chain.length; + defaultOp[0] = field; + field = 'name_me_' + num; + } + $scope.params[name].push([field, defaultOp]); + $scope.controls[name] = null; + } + }); + }); + } + }); + $scope.availableParams = actionInfo.params; + } + writeCode(); + } + + function defaultValues(defaultVal) { + _.each($scope.fields, function(field) { + if (field.required) { + defaultVal.push([field.id, '']); + } + }); + return defaultVal; + } + + function stringify(value, trim) { + if (typeof value === 'undefined') { + return ''; + } + var str = JSON.stringify(value).replace(/,/g, ', '); + if (trim) { + str = str.slice(1, -1); + } + return str.trim(); + } + + function writeCode() { + var code = { + php: ts('Select an entity and action'), + javascript: '', + cli: '' + }, + entity = $scope.entity, + action = $scope.action, + params = getParams(), + index = isInt($scope.index) ? +$scope.index : $scope.index, + result = 'result'; + if ($scope.entity && $scope.action) { + if (action.slice(0, 3) === 'get') { + result = entity.substr(0, 7) === 'Custom_' ? _.camelCase(entity.substr(7)) : entity; + result = lcfirst(action.replace(/s$/, '').slice(3) || result); + } + var results = lcfirst(_.isNumber(index) ? result : pluralize(result)), + paramCount = _.size(params), + i = 0; + + // Write javascript + code.javascript = "CRM.api4('" + entity + "', '" + action + "', {"; + _.each(params, function(param, key) { + code.javascript += "\n " + key + ': ' + stringify(param) + + (++i < paramCount ? ',' : ''); + if (key === 'checkPermissions') { + code.javascript += ' // IGNORED: permissions are always enforced from client-side requests'; + } + }); + code.javascript += "\n}"; + if (index || index === 0) { + code.javascript += ', ' + JSON.stringify(index); + } + code.javascript += ").then(function(" + results + ") {\n // do something with " + results + " array\n}, function(failure) {\n // handle failure\n});"; + + // Write php code + if (entity.substr(0, 7) !== 'Custom_') { + code.php = '$' + results + " = \\Civi\\Api4\\" + entity + '::' + action + '()'; + } else { + code.php = '$' + results + " = \\Civi\\Api4\\CustomValue::" + action + "('" + entity.substr(7) + "')"; + } + _.each(params, function(param, key) { + var val = ''; + if (typeof objectParams[key] !== 'undefined' && key !== 'chain') { + _.each(param, function(item, index) { + val = phpFormat(index) + ', ' + phpFormat(item, 4); + code.php += "\n ->add" + ucfirst(key).replace(/s$/, '') + '(' + val + ')'; + }); + } else if (key === 'where') { + _.each(param, function (clause) { + if (clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT') { + code.php += "\n ->addClause(" + phpFormat(clause[0]) + ", " + phpFormat(clause[1]).slice(1, -1) + ')'; + } else { + code.php += "\n ->addWhere(" + phpFormat(clause).slice(1, -1) + ")"; + } + }); + } else { + code.php += "\n ->set" + ucfirst(key) + '(' + phpFormat(param, 4) + ')'; + } + }); + code.php += "\n ->execute()"; + if (_.isNumber(index)) { + code.php += !index ? '\n ->first()' : (index === -1 ? '\n ->last()' : '\n ->itemAt(' + index + ')'); + } else if (index) { + code.php += "\n ->indexBy('" + index + "')"; + } + code.php += ";\n"; + if (!_.isNumber(index)) { + code.php += "foreach ($" + results + ' as $' + ((_.isString(index) && index) ? index + ' => $' : '') + result + ') {\n // do something\n}'; + } + + // Write cli code + code.cli = 'cv api4 ' + entity + '.' + action + " '" + stringify(params) + "'"; + } + $scope.code = code; + } + + function isInt(value) { + if (_.isNumber(value)) { + return true; + } + if (!_.isString(value)) { + return false; + } + return /^-{0,1}\d+$/.test(value); + } + + $scope.execute = function() { + $scope.status = 'warning'; + $scope.loading = true; + crmApi4($scope.entity, $scope.action, getParams(), $scope.index) + .then(function(data) { + var meta = {length: _.size(data)}, + result = JSON.stringify(data, null, 2); + if (_.isArray(data)) { + data.length = 0; + _.assign(meta, data); + } + $scope.loading = false; + $scope.status = 'success'; + $scope.result = [JSON.stringify(meta).replace('{', '').replace(/}$/, ''), result]; + }, function(data) { + $scope.loading = false; + $scope.status = 'danger'; + $scope.result = [JSON.stringify(data, null, 2)]; + }); + }; + + /** + * Format value to look like php code + */ + function phpFormat(val, indent) { + if (typeof val === 'undefined') { + return ''; + } + indent = (typeof indent === 'number') ? _.repeat(' ', indent) : (indent || ''); + var ret = '', + baseLine = indent ? indent.slice(0, -2) : '', + newLine = indent ? '\n' : ''; + if ($.isPlainObject(val)) { + $.each(val, function(k, v) { + ret += (ret ? ', ' : '') + newLine + indent + "'" + k + "' => " + phpFormat(v); + }); + return '[' + ret + newLine + baseLine + ']'; + } + if ($.isArray(val)) { + $.each(val, function(k, v) { + ret += (ret ? ', ' : '') + newLine + indent + phpFormat(v); + }); + return '[' + ret + newLine + baseLine + ']'; + } + if (_.isString(val) && !_.contains(val, "'")) { + return "'" + val + "'"; + } + return JSON.stringify(val).replace(/\$/g, '\\$'); + } + + function fetchMeta() { + crmApi4(getMetaParams) + .then(function(data) { + if (data.schema) { + schema = data.schema; + entities.length = 0; + formatForSelect2(schema, entities, 'name', ['description']); + if ($scope.entity && !$scope.action) { + showEntityHelp($scope.entity); + } + } + if (data.links) { + links = data.links; + } + if (data.actions) { + getEntity().actions = data.actions; + selectAction(); + } + }); + } + + // Help for an entity with no action selected + function showEntityHelp(entityName) { + var entityInfo = getEntity(entityName); + $scope.helpTitle = helpTitle = $scope.entity; + $scope.helpContent = helpContent = { + description: entityInfo.description, + comment: entityInfo.comment + }; + } + + if (!$scope.entity) { + $scope.helpTitle = helpTitle = ts('Help'); + $scope.helpContent = helpContent = {description: ts('Welcome to the api explorer.'), comment: ts('Select an entity to begin.')}; + if (getMetaParams.schema) { + fetchMeta(); + } + } else if (!actions.length && (!schema.length || !getEntity().actions)) { + if (getMetaParams.schema) { + entities.push({id: $scope.entity, text: $scope.entity}); + } + getMetaParams.actions = [$scope.entity, 'getActions', {chain: {fields: [$scope.entity, 'getFields', {action: '$name'}]}}]; + fetchMeta(); + } else { + selectAction(); + } + + if ($scope.entity && schema.length) { + showEntityHelp($scope.entity); + } + + // Update route when changing entity + $scope.$watch('entity', function(newVal, oldVal) { + if (oldVal !== newVal) { + // Flush actions cache to re-fetch for new entity + actions = []; + $location.url('/explorer/' + newVal); + } + }); + + // Update route when changing actions + $scope.$watch('action', function(newVal, oldVal) { + if ($scope.entity && $routeParams.api4action !== newVal && !_.isUndefined(newVal)) { + $location.url('/explorer/' + $scope.entity + '/' + newVal); + } else if (newVal) { + $scope.helpTitle = helpTitle = $scope.entity + '::' + newVal; + $scope.helpContent = helpContent = _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment']); + } + }); + + $scope.indexHelp = { + description: ts('(string|int) Index results or select by index.'), + comment: ts('Pass a string to index the results by a field value. E.g. index: "name" will return an associative array with names as keys.') + '\n\n' + + ts('Pass an integer to return a single result; e.g. index: 0 will return the first result, 1 will return the second, and -1 will return the last.') + }; + + $scope.$watch('params', writeCode, true); + $scope.$watch('index', writeCode); + writeCode(); + + }); + + angular.module('api4Explorer').directive('crmApi4WhereClause', function($timeout) { + return { + scope: { + data: '=crmApi4WhereClause' + }, + templateUrl: '~/api4Explorer/WhereClause.html', + link: function (scope, element, attrs) { + var ts = scope.ts = CRM.ts('api4'); + scope.newClause = ''; + scope.conjunctions = ['AND', 'OR', 'NOT']; + scope.operators = CRM.vars.api4.operators; + + scope.addGroup = function(op) { + scope.data.where.push([op, []]); + }; + + scope.removeGroup = function() { + scope.data.groupParent.splice(scope.data.groupIndex, 1); + }; + + scope.onSort = function(event, ui) { + $('.api4-where-fieldset').toggleClass('api4-sorting', event.type === 'sortstart'); + $('.api4-input.form-inline').css('margin-left', ''); + }; + + // Indent clause while dragging between nested groups + scope.onSortOver = function(event, ui) { + var offset = 0; + if (ui.sender) { + offset = $(ui.placeholder).offset().left - $(ui.sender).offset().left; + } + $('.api4-input.form-inline.ui-sortable-helper').css('margin-left', '' + offset + 'px'); + }; + + scope.$watch('newClause', function(value) { + var field = value; + $timeout(function() { + if (field) { + scope.data.where.push([field, '=', '']); + scope.newClause = null; + } + }); + }); + scope.$watch('data.where', function(values) { + // Remove empty values + _.each(values, function(clause, index) { + if (typeof clause !== 'undefined' && !clause[0]) { + values.splice(index, 1); + } + }); + }, true); + } + }; + }); + + angular.module('api4Explorer').directive('api4ExpValue', function($routeParams, crmApi4) { + return { + scope: { + data: '=api4ExpValue' + }, + link: function (scope, element, attrs) { + var ts = scope.ts = CRM.ts('api4'), + entity = $routeParams.api4entity; + + function getField(fieldName) { + var fieldNames = fieldName.split('.'); + return get(entity, fieldNames); + + function get(entity, fieldNames) { + if (fieldNames.length === 1) { + return _.findWhere(entityFields(entity), {name: fieldNames[0]}); + } + var comboName = _.findWhere(entityFields(entity), {name: fieldNames[0] + '.' + fieldNames[1]}); + if (comboName) { + return comboName; + } + var linkName = fieldNames.shift(), + entityLinks = _.findWhere(links, {entity: entity}).links, + newEntity = _.findWhere(entityLinks, {alias: linkName}).entity; + return get(newEntity, fieldNames); + } + } + + function destroyWidget() { + var $el = $(element); + if ($el.is('.crm-form-date-wrapper .crm-hidden-date')) { + $el.crmDatepicker('destroy'); + } + if ($el.is('.select2-container + input')) { + $el.crmEntityRef('destroy'); + } + $(element).removeData().removeAttr('type').removeAttr('placeholder').show(); + } + + function makeWidget(field, op) { + var $el = $(element), + dataType = field.data_type, + multi = _.includes(['IN', 'NOT IN'], op); + if (op === 'IS NULL' || op === 'IS NOT NULL') { + $el.hide(); + return; + } + if (dataType === 'Timestamp' || dataType === 'Date') { + if (_.includes(['=', '!=', '<>', '<', '>=', '<', '<='], op)) { + $el.crmDatepicker({time: dataType === 'Timestamp'}); + } + } else if (_.includes(['=', '!=', '<>', 'IN', 'NOT IN'], op)) { + if (field.fk_entity) { + $el.crmEntityRef({entity: field.fk_entity, select:{multiple: multi}}); + } else if (field.options) { + $el.addClass('loading').attr('placeholder', ts('- select -')).crmSelect2({multiple: multi, data: [{id: '', text: ''}]}); + loadFieldOptions(field.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}); + }); + } else if (dataType === 'Boolean') { + $el.attr('placeholder', ts('- select -')).crmSelect2({allowClear: false, multiple: multi, placeholder: ts('- select -'), data: [ + {id: '1', text: ts('Yes')}, + {id: '0', text: ts('No')} + ]}); + } + } + } + + function loadFieldOptions(entity) { + var action = $routeParams.api4action; + if (!fieldOptions[entity + action]) { + fieldOptions[entity + action] = crmApi4(entity, 'getFields', { + loadOptions: true, + action: action, + where: [["options", "!=", false]], + select: ["name", "options"] + }); + } + return fieldOptions[entity + action]; + } + + scope.$watchCollection('data', function(data) { + destroyWidget(); + var field = getField(data.field); + if (field) { + makeWidget(field, data.op || '='); + } + }); + } + }; + }); + + + angular.module('api4Explorer').directive('api4ExpChain', function(crmApi4) { + return { + scope: { + chain: '=api4ExpChain', + mainEntity: '=', + entities: '=' + }, + templateUrl: '~/api4Explorer/Chain.html', + link: function (scope, element, attrs) { + var ts = scope.ts = CRM.ts('api4'); + + function changeEntity(newEntity, oldEntity) { + // When clearing entity remove this chain + if (!newEntity) { + scope.chain[0] = ''; + return; + } + // Reset action && index + if (newEntity !== oldEntity) { + scope.chain[1][1] = scope.chain[1][2] = ''; + } + if (getEntity(newEntity).actions) { + setActions(); + } else { + crmApi4(newEntity, 'getActions', {chain: {fields: [newEntity, 'getFields', {action: '$name'}]}}) + .then(function(data) { + getEntity(data.entity).actions = data; + if (data.entity === scope.chain[1][0]) { + setActions(); + } + }); + } + } + + function setActions() { + scope.actions = [''].concat(_.pluck(getEntity(scope.chain[1][0]).actions, 'name')); + } + + // Set default params when choosing action + function changeAction(newAction, oldAction) { + var link; + // Prepopulate links + if (newAction && newAction !== oldAction) { + // Clear index + scope.chain[1][3] = ''; + // Look for links back to main entity + _.each(entityFields(scope.chain[1][0]), function(field) { + if (field.fk_entity === scope.mainEntity) { + link = [field.name, '$id']; + } + }); + // Look for links from main entity + if (!link && newAction !== 'create') { + _.each(entityFields(scope.mainEntity), function(field) { + if (field.fk_entity === scope.chain[1][0]) { + link = ['id', '$' + field.name]; + // Since we're specifying the id, set index to getsingle + scope.chain[1][3] = '0'; + } + }); + } + if (link && _.contains(['get', 'update', 'replace', 'delete'], newAction)) { + scope.chain[1][2] = '{where: [[' + link[0] + ', =, ' + link[1] + ']]}'; + } + else if (link && _.contains(['create'], newAction)) { + scope.chain[1][2] = '{values: {' + link[0] + ': ' + link[1] + '}}'; + } else { + scope.chain[1][2] = '{}'; + } + } + } + + scope.$watch("chain[1][0]", changeEntity); + scope.$watch("chain[1][1]", changeAction); + } + }; + }); + + function getEntity(entityName) { + return _.findWhere(schema, {name: entityName}); + } + + function entityFields(entityName) { + return _.result(getEntity(entityName), 'fields'); + } + + // Collapsible optgroups for select2 + $(function() { + $('body') + .on('select2-open', function(e) { + if ($(e.target).hasClass('collapsible-optgroups')) { + $('#select2-drop') + .off('.collapseOptionGroup') + .addClass('collapsible-optgroups-enabled') + .on('click.collapseOptionGroup', '.select2-result-with-children > .select2-result-label', function() { + $(this).parent().toggleClass('optgroup-expanded'); + }); + } + }) + .on('select2-close', function() { + $('#select2-drop').off('.collapseOptionGroup').removeClass('collapsible-optgroups-enabled'); + }); + }); +})(angular, CRM.$, CRM._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/WhereClause.html b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/WhereClause.html new file mode 100644 index 00000000..d36480f9 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/WhereClause.html @@ -0,0 +1,39 @@ +<legend>{{ data.label || data.op + ' group' }}<span class="crm-marker" ng-if="data.required"> *</span></legend> +<div class="btn-group btn-group-xs" ng-if="data.groupParent"> + <button class="btn btn-danger-outline" ng-click="removeGroup()" title="{{ ts('Remove group') }}"> + <i class="crm-i fa-trash"></i> + </button> +</div> +<div class="api4-where-group-sortable" ng-model="data.where" ui-sortable="{axis: 'y', connectWith: '.api4-where-group-sortable', containment: '.api4-where-fieldset', over: onSortOver, start: onSort, stop: onSort}"> + <div class="api4-input form-inline clearfix" ng-repeat="(index, clause) in data.where"> + <div class="api4-clause-badge" title="{{ ts('Drag to reposition') }}"> + <span class="badge badge-info"> + <span ng-if="!index && !data.groupParent">Where</span> + <span ng-if="index || data.groupParent">{{ data.op }}</span> + <i class="crm-i fa-arrows"></i> + </span> + </div> + <div ng-if="clause[0] !== 'AND' && clause[0] !== 'OR' && clause[0] !== 'NOT'" class="api4-input-group"> + <input class="collapsible-optgroups form-control" ng-model="clause[0]" crm-ui-select="{data: data.fields, allowClear: true, placeholder: 'Field'}" /> + <select class="form-control api4-operator" ng-model="clause[1]" ng-options="o for o in operators" ></select> + <input class="form-control" ng-model="clause[2]" api4-exp-value="{field: clause[0], op: clause[1]}" /> + </div> + <fieldset class="clearfix" ng-if="clause[0] === 'AND' || clause[0] === 'OR' || clause[0] === 'NOT'" crm-api4-where-clause="{where: clause[1], op: clause[0], fields: data.fields, operators: data.operators, groupParent: data.where, groupIndex: index}"> + </fieldset> + </div> +</div> +<div class="api4-input form-inline"> + <div class="api4-clause-badge"> + <div class="btn-group btn-group-xs" title="{{ data.groupParent ? ts('Add a subgroup of clauses') : ts('Add a group of clauses') }}"> + <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {{ data.op }} <span class="caret"></span> + </button> + <ul class="dropdown-menu api4-add-where-group-menu"> + <li ng-repeat="con in conjunctions" ng-if="data.op !== con"> + <a href ng-click="addGroup(con)">{{ con }}</a> + </li> + </ul> + </div> + </div> + <input class="collapsible-optgroups form-control" ng-model="newClause" title="Add a single clause" crm-ui-select="{data: data.fields, placeholder: 'Add clause'}" /> +</div>
\ No newline at end of file |