'force' => 1,
'qfKey' => $qfKey,
];
- if ($query->_context === 'amtg') {
+ if (($query->_context ?? '') === 'amtg') {
// See https://lab.civicrm.org/dev/core/-/issues/2333
// Seems to be needed in add to group flow.
$urlParams['_qf_Basic_display'] = 1;
*/
public function boot($apiRequest) {
require_once 'api/Exception.php';
+ // the create error function loads some functions from utils
+ // so this require is also needed for apiv4 until such time as
+ // we alter create error.
+ require_once 'api/v3/utils.php';
switch ($apiRequest['version']) {
case 3:
if (!is_array($apiRequest['params'])) {
throw new \API_Exception('Input variable `params` is not an array', 2000);
}
- require_once 'api/v3/utils.php';
_civicrm_api3_initialize();
break;
public function run() {
$breadCrumb = [
- 'title' => ts('Forms'),
+ 'title' => ts('Form Builder'),
'url' => CRM_Utils_System::url('civicrm/admin/afform', NULL, FALSE, '/'),
];
CRM_Utils_System::appendBreadCrumb([$breadCrumb]);
if (!$existing) {
civicrm_api3('Navigation', 'create', [
'parent_id' => 'Customize Data and Screens',
- 'label' => E::ts('Forms'),
+ 'label' => E::ts('Form Builder'),
'weight' => 1,
- 'name' => 'afform_gui',
+ 'name' => 'afform_admin',
'permission' => 'administer CiviCRM',
'url' => 'civicrm/admin/afform',
'is_active' => 1,
+ 'icon' => 'crm-i fa-list-alt',
]);
}
}
]);
}
+ /**
+ * Update menu item
+ *
+ * @return TRUE on success
+ * @throws Exception
+ */
+ public function upgrade_0001() {
+ $this->ctx->log->info('Applying update 0001');
+ \Civi\Api4\Navigation::update(FALSE)
+ ->addValue('icon', 'crm-i fa-list-alt')
+ ->addValue('label', E::ts('Form Builder'))
+ ->addValue('name', 'afform_admin')
+ ->addWhere('name', '=', 'afform_gui')
+ ->execute();
+ return TRUE;
+ }
+
}
* @return array
*/
public static function getAdminSettings() {
+ $afformTypes = (array) \Civi\Api4\OptionValue::get(FALSE)
+ ->addSelect('name', 'label', 'icon')
+ ->addWhere('is_active', '=', TRUE)
+ ->addWhere('option_group_id:name', '=', 'afform_type')
+ ->addOrderBy('weight', 'ASC')
+ ->execute();
+ // Pluralize tabs (too bad option groups only store a single label)
+ $plurals = [
+ 'form' => ts('Custom Forms'),
+ 'search' => ts('Search Displays'),
+ 'block' => ts('Field Blocks'),
+ 'system' => ts('System Forms'),
+ ];
+ foreach ($afformTypes as $index => $type) {
+ $afformTypes[$index]['plural'] = $plurals[$type['name']] ?? \CRM_Utils_String::pluralize($type['label']);
+ }
return [
- 'afform_type' => \Civi\Api4\OptionValue::get(FALSE)
- ->addSelect('name', 'label', 'icon')
- ->addWhere('is_active', '=', TRUE)
- ->addWhere('option_group_id:name', '=', 'afform_type')
- ->addOrderBy('weight', 'ASC')
- ->execute(),
+ 'afform_type' => $afformTypes,
];
}
// The full contents of blocks used on the form have been loaded. Get basic info about others relevant to these entities.
$blockInfo = Afform::get($this->checkPermissions)
- ->addSelect('name', 'title', 'block', 'join', 'directive_name')
+ ->addSelect('name', 'title', 'block', 'join', 'directive_name', 'repeat')
->addWhere('type', '=', 'block')
->addWhere('block', 'IN', $entities)
->addWhere('directive_name', 'NOT IN', array_keys($info['blocks']))
$scope.crmUrl = CRM.url;
this.tabs = CRM.afAdmin.afform_type;
- $scope.tabs = _.indexBy(ctrl.tabs, 'name');
+ $scope.types = _.indexBy(ctrl.tabs, 'name');
_.each(['form', 'block', 'search'], function(type) {
- if ($scope.tabs[type]) {
- $scope.tabs[type].options = [];
+ if ($scope.types[type]) {
+ $scope.types[type].options = [];
if (type === 'form') {
- $scope.tabs.form.default = '#create/form/Individual';
+ $scope.types.form.default = '#create/form/Individual';
}
}
});
this.createLinks = function() {
ctrl.searchCreateLinks = '';
- if ($scope.tabs[ctrl.tab].options.length) {
+ if ($scope.types[ctrl.tab].options.length) {
return;
}
var links = [];
});
}
});
- $scope.tabs.form.options = _.sortBy(links, 'Label');
+ $scope.types.form.options = _.sortBy(links, 'Label');
}
if (ctrl.tab === 'block') {
});
}
});
- $scope.tabs.block.options = _.sortBy(links, 'Label');
+ $scope.types.block.options = _.sortBy(links, 'Label');
}
if (ctrl.tab === 'search') {
icon: searchDisplay['type:icon']
});
});
- $scope.tabs.search.options = _.sortBy(links, 'Label');
+ $scope.types.search.options = _.sortBy(links, 'Label');
});
}
};
<div id="bootstrap-theme" class="afadmin-list">
- <h1 crm-page-title>{{:: ts('Configurable Forms') }}</h1>
+ <h1 crm-page-title>{{:: ts('Form Builder') }}</h1>
<ul class="nav nav-tabs">
<li role="presentation" ng-repeat="tab in $ctrl.tabs" ng-class="{active: tab.name === $ctrl.tab}">
- <a href ng-click="$ctrl.tab = tab.name; $ctrl.searchAfformList = ''"><i class="crm-i {{ tab.icon }}"></i> {{:: tab.label }}</a>
+ <a href ng-click="$ctrl.tab = tab.name; $ctrl.searchAfformList = ''"><i class="crm-i {{ tab.icon }}"></i> {{:: tab.plural }}</a>
</li>
</ul>
<div class="form-inline">
<label for="afform-list-filter">{{:: ts('Filter:') }}</label>
<input class="form-control" type="search" id="afform-list-filter" ng-model="$ctrl.searchAfformList" placeholder="">
- <div class="btn-group pull-right" ng-if="tabs[$ctrl.tab].options">
- <a ng-if="tabs[$ctrl.tab].default" href="{{ tabs[$ctrl.tab].default }}" class="btn btn-primary">
- {{ ts('New %1', {1: tabs[$ctrl.tab].label }) }}
+ <div class="btn-group pull-right" ng-if="types[$ctrl.tab].options">
+ <a ng-if="types[$ctrl.tab].default" href="{{ types[$ctrl.tab].default }}" class="btn btn-primary">
+ {{ ts('New %1', {1: types[$ctrl.tab].label }) }}
</a>
<button ng-click="$ctrl.createLinks()" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <span ng-class="{'sr-only': tabs[$ctrl.tab].default}">{{ ts('New %1', {1: tabs[$ctrl.tab].label }) }}</span>
+ <span ng-class="{'sr-only': types[$ctrl.tab].default}">{{ ts('New %1', {1: types[$ctrl.tab].label }) }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
- <input ng-if="tabs[$ctrl.tab].options.length" type="search" class="form-control" placeholder="" ng-model="$ctrl.searchCreateLinks">
- <a href ng-if="!tabs[$ctrl.tab].options.length"><i class="crm-i fa-spinner fa-spin"></i></a>
+ <input ng-if="types[$ctrl.tab].options.length" type="search" class="form-control" placeholder="" ng-model="$ctrl.searchCreateLinks">
+ <a href ng-if="!types[$ctrl.tab].options.length"><i class="crm-i fa-spinner fa-spin"></i></a>
</li>
- <li ng-repeat="link in tabs[$ctrl.tab].options | filter:$ctrl.searchCreateLinks">
+ <li ng-repeat="link in types[$ctrl.tab].options | filter:$ctrl.searchCreateLinks">
<a href="{{ link.url }}">
<i class="crm-i {{ link.icon }}"></i>
{{ link.label }}
}
if (editor.mode === 'clone') {
delete $scope.afform.name;
+ delete $scope.afform.server_route;
+ $scope.afform.is_dashlet = false;
$scope.afform.title += ' ' + ts('(copy)');
}
$scope.canvasTab = 'layout';
};
$scope.save = function() {
+ var afform = JSON.parse(angular.toJson($scope.afform));
+ // This might be set to undefined by validation
+ afform.server_route = afform.server_route || '';
$scope.saving = $scope.changesSaved = true;
- crmApi4('Afform', 'save', {formatWhitespace: true, records: [JSON.parse(angular.toJson($scope.afform))]})
+ crmApi4('Afform', 'save', {formatWhitespace: true, records: [afform]})
.then(function (data) {
$scope.saving = false;
$scope.afform.name = data[0].name;
<label for="af_config_form_title">
{{:: ts('Title:') }} <span class="crm-marker">*</span>
</label>
- <input ng-model="afform.title" class="form-control" id="af_config_form_title" required />
+ <input ng-model="afform.title" class="form-control" id="af_config_form_title" required title="{{:: ts('Required') }}" />
</div>
<div class="form-group">
<!-- "Semi-private": not generally public, but not audited for secrecy -->
</div>
-<div class="form-group">
+<div class="form-group" ng-class="{'has-error': !!config_form.server_route.$error.pattern}">
<label for="af_config_form_server_route">
- {{:: ts('Path:') }}
+ {{:: ts('Page:') }}
</label>
- <input ng-model="afform.server_route" class="form-control" id="af_config_form_server_route" />
- <p class="help-block">{{:: ts('Expose the form as a standalone page on the web site. (Example: "civicrm/my-form")') }}</p>
+ <input ng-model="afform.server_route" name="server_route" class="form-control" id="af_config_form_server_route" pattern="^civicrm\/[-0-9a-zA-z\/_]+$" onfocus="this.value = this.value || 'civicrm/'" onblur="if (this.value === 'civicrm/') this.value = ''" title="{{:: ts('Path must begin with "civicrm/"') }}">
+ <p class="help-block">{{:: ts('Expose the form as a standalone webpage. (Example: "civicrm/my-form")') }}</p>
</div>
<div class="form-group" ng-if="!!afform.server_route">
}
$customApi = CustomGroup::get()
->setCheckPermissions(FALSE)
- ->setSelect(['name', 'title', 'help_pre', 'help_post', 'extends'])
+ ->setSelect(['name', 'title', 'help_pre', 'help_post', 'extends', 'max_multiple'])
->addWhere('is_multiple', '=', 1)
->addWhere('is_active', '=', 1);
if ($groupNames) {
'permission' => 'access CiviCRM',
'join' => 'Custom_' . $custom['name'],
'block' => $custom['extends'],
- 'repeat' => TRUE,
+ 'repeat' => $custom['max_multiple'] ?: TRUE,
'has_base' => TRUE,
];
if ($getLayout) {
foreach ($this->values[$entityName] ?? [] as $values) {
$entityValues[$entity['type']][$entityName][] = $values + ['fields' => []];
// Predetermined values override submitted values
- if (!empty($entity['af-values'])) {
+ if (!empty($entity['data'])) {
foreach ($entityValues[$entity['type']][$entityName] as $index => $vals) {
- $entityValues[$entity['type']][$entityName][$index]['fields'] = $entity['af-values'] + $vals['fields'];
+ $entityValues[$entity['type']][$entityName][$index]['fields'] = $entity['data'] + $vals['fields'];
}
}
}
'oauthClientRedirectUrl' => [
'group_name' => 'Developer Preferences',
'group' => 'developer',
- 'name' => 'fatalErrorHandler',
+ 'name' => 'oauthClientRedirectUrl',
'type' => 'String',
'quick_form_type' => 'Element',
'html_type' => 'text',
return;
}
var splitAs = expr.split(' AS '),
- info = {fn: null, modifier: ''},
+ info = {fn: null, modifier: '', field: {}},
fieldName = splitAs[0],
bracketPos = splitAs[0].indexOf('(');
if (bracketPos >= 0) {
$scope.fieldsForHaving = function() {
return {results: _.transform(ctrl.savedSearch.api_params.select, function(fields, name) {
- fields.push({id: name, text: ctrl.getFieldLabel(name)});
+ var info = searchMeta.parseExpr(name);
+ fields.push({id: info.alias + info.suffix, text: ctrl.getFieldLabel(name)});
})};
};
</div>
</td>
<td>{{ search.groups.join(', ') }}</td>
- <td ng-if="$ctrl.afformEnabled">
+ <td ng-if="::$ctrl.afformEnabled">
<div class="btn-group">
<button type="button" ng-click="$ctrl.loadAfforms()" ng-if="search.display_name" class="btn btn-xs dropdown-toggle btn-primary-outline" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- {{:: ts('Forms') }} <span class="caret"></span>
+ {{ $ctrl.afforms ? ($ctrl.afforms[search.name] && $ctrl.afforms[search.name].length === 1 ? ts('1 Form') : ts('%1 Forms', {1: $ctrl.afforms[search.name] ? $ctrl.afforms[search.name].length : 0})) : ts('Forms...') }}
+ <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="display_name in search.display_name" ng-if="::$ctrl.afformAdminEnabled">
<em ng-if="$ctrl.afforms && !$ctrl.afforms[search.name]">{{:: ts('None Found') }}</em>
</a>
</li>
- <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[search.name]" ng-class="{disabled: !afform.url}">
+ <li ng-if="$ctrl.afforms" ng-repeat="afform in $ctrl.afforms[search.name]" ng-class="{disabled: !afform.url}" title="{{:: afform.url ? ts('Open form in new tab') : ts('This form does not have a page') }}">
<a href="{{:: afform.url }}" target="_blank">
+ <i class="crm-i {{:: afform.url ? 'fa-external-link' : 'fa-list-alt' }}"></i>
{{:: afform.title }}
</a>
</li>