summaryrefslogtreecommitdiff
path: root/www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang
diff options
context:
space:
mode:
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang')
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.ang.php12
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4.js4
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4/crmApi4.js37
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.ang.php18
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer.js4
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Chain.html4
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.html137
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/Explorer.js789
-rw-r--r--www/crm/wp-content/plugins/civicrm/civicrm/ext/api4/ang/api4Explorer/WhereClause.html39
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