Commit 79e72ae9 authored by colemanw's avatar colemanw
Browse files

GUI - present Individual/Organization/Household as seperate entities

Also adds per-contact-type default name blocks.
parent 4b1e2da8
<div class="af-container af-layout-inline">
<af-field name="household_name" />
<af-field name="nick_name" />
</div>
{
"title": "Household Name (default)",
"block": "Household"
}
{
"title": "Individual Name (default)",
"block": "Contact"
"block": "Individual"
}
<div class="af-container af-layout-inline">
<af-field name="organization_name" />
<af-field name="legal_name" />
<af-field name="nick_name" />
</div>
{
"title": "Organization Name (default)",
"block": "Organization"
}
<?php
return [
'entity' => 'Activity',
'label' => ts('Activity'),
'defaults' => "{'url-autofill': '1'}",
];
<?php
return [
'entity' => 'Contact',
'contact_type' => 'Household',
'defaults' => "{
data: {
contact_type: 'Household',
source: afform.title
},
'url-autofill': '1'
}",
];
<?php
return [
'entity' => 'Contact',
'contact_type' => 'Individual',
'defaults' => "{
data: {
contact_type: 'Individual',
source: afform.title
},
'url-autofill': '1'
}",
];
<?php
return [
'entity' => 'Contact',
'contact_type' => 'Organization',
'defaults' => "{
data: {
contact_type: 'Organization',
source: afform.title
},
'url-autofill': '1'
}",
];
......@@ -162,27 +162,51 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
return;
}
$entityWhitelist = $data = [];
$getFieldParams = [
'checkPermissions' => FALSE,
'includeCustom' => TRUE,
'loadOptions' => TRUE,
'action' => 'create',
'select' => ['name', 'title', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type'],
'where' => [['input_type', 'IS NOT NULL']],
];
// First scan the entityDefaults directory for our list of supported entities
// FIXME: Need a way to load this from other extensions too
foreach (glob(__DIR__ . '/ang/afGuiEditor/entityDefaults/*.json') as $file) {
$matches = [];
preg_match('/([-a-z_A-Z0-9]*).json/', $file, $matches);
$entityWhitelist[] = $entity = $matches[1];
// No json_decode, the files are not strict json and will go through angular.$parse clientside
$data['defaults'][$entity] = trim(CRM_Utils_JS::stripComments(file_get_contents($file)));
}
$data = [
'entities' => [
'Contact' => [
'entity' => 'Contact',
'label' => ts('Contact'),
'fields' => (array) civicrm_api4('Contact', 'getFields', $getFieldParams, 'name'),
],
],
'blocks' => [],
];
// Load main entities
$data['entities'] = (array) Civi\Api4\Entity::get()
->setCheckPermissions(FALSE)
->setSelect(['name', 'description'])
->addWhere('name', 'IN', $entityWhitelist)
->execute();
$contactTypes = CRM_Contact_BAO_ContactType::basicTypeInfo();
// Scan all extensions for our list of supported entities
foreach (CRM_Extension_System::singleton()->getMapper()->getActiveModuleFiles() as $ext) {
$dir = CRM_Utils_File::addTrailingSlash(dirname($ext['filePath'])) . 'afformEntities';
if (is_dir($dir)) {
foreach (glob($dir . '/*.php') as $file) {
$entity = include $file;
// Skip disabled contact types
if (!empty($entity['contact_type']) && !isset($contactTypes[$entity['contact_type']])) {
continue;
}
if (!empty($entity['contact_type'])) {
$entity['label'] = $contactTypes[$entity['contact_type']]['label'];
}
// For Contact pseudo-entities (Individual, Organization, Household)
$values = array_intersect_key($entity, ['contact_type' => NULL]);
$afformEntity = $entity['contact_type'] ?? $entity['entity'];
$entity['fields'] = (array) civicrm_api4($entity['entity'], 'getFields', $getFieldParams + ['values' => $values], 'name');
$data['entities'][$afformEntity] = $entity;
}
}
}
// Load blocks
$data['blocks'] = [];
$blockData = \Civi\Api4\Afform::get()
->setCheckPermissions(FALSE)
->addWhere('block', 'IS NOT NULL')
......@@ -191,8 +215,11 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
->setLayoutFormat('shallow')
->execute();
foreach ($blockData as $block) {
if (!empty($block['join']) && !in_array($block['join'], $entityWhitelist)) {
$entityWhitelist[] = $block['join'];
if (!empty($block['join']) && !isset($data['entities'][$block['join']]['fields'])) {
$data['entities'][$block['join']]['entity'] = $block['join'];
// Normally you shouldn't pass variables to ts() but very common strings like "Email" should already exist
$data['entities'][$block['join']]['label'] = ts($block['join']);
$data['entities'][$block['join']]['fields'] = (array) civicrm_api4($block['join'], 'getFields', $getFieldParams, 'name');
}
$data['blocks'][_afform_angular_module_name($block['name'], 'dash')] = $block;
}
......@@ -239,34 +266,19 @@ function afform_gui_civicrm_buildAsset($asset, $params, &$mimeType, &$content) {
],
];
$getFieldParams = [
'checkPermissions' => FALSE,
'includeCustom' => TRUE,
'loadOptions' => TRUE,
'action' => 'create',
'select' => ['name', 'title', 'input_type', 'input_attrs', 'required', 'options', 'help_pre', 'help_post', 'serialize', 'data_type'],
'where' => [['input_type', 'IS NOT NULL']],
];
// Get fields for main entities + joined entities
foreach (array_unique($entityWhitelist) as $entityName) {
$data['fields'][$entityName] = (array) civicrm_api4($entityName, 'getFields', $getFieldParams, 'name');
// TODO: Teach the api to return options in this format
foreach ($data['fields'][$entityName] as $name => $field) {
// Reformat options
// TODO: Teach the api to return options in this format
foreach ($data['entities'] as $entityName => $entity) {
foreach ($entity['fields'] as $name => $field) {
if (!empty($field['options'])) {
$data['fields'][$entityName][$name]['options'] = CRM_Utils_Array::makeNonAssociative($field['options'], 'key', 'label');
$data['entities'][$entityName]['fields'][$name]['options'] = CRM_Utils_Array::makeNonAssociative($field['options'], 'key', 'label');
}
else {
unset($data['fields'][$entityName][$name]['options']);
unset($data['entities'][$entityName]['fields'][$name]['options']);
}
}
}
// Now adjust the field metadata
// FIXME: This should probably be a callback event or something to allow extensions to tweak the metadata for their entities
$data['fields']['Contact']['contact_type']['required_data'] = TRUE;
// Scan for input types
// FIXME: Need a way to load this from other extensions too
foreach (glob(__DIR__ . '/ang/afGuiEditor/inputType/*.html') as $file) {
......
......@@ -51,7 +51,7 @@
else {
$timeout(function() {
initialize(_.cloneDeep(newForm));
editor.addEntity('Contact');
editor.addEntity('Individual');
$scope.layout['#children'].push({
"#tag": "button",
"class": 'af-button btn btn-primary',
......@@ -79,38 +79,42 @@
}, true);
}
this.addEntity = function(entityType) {
var existingEntitiesofThisType = _.map(_.filter($scope.entities, {type: entityType}), 'name'),
num = existingEntitiesofThisType.length + 1;
this.addEntity = function(type) {
var meta = editor.meta.entities[type],
num = 1;
// Give this new entity a unique name
while (_.contains(existingEntitiesofThisType, entityType + num)) {
while (!!$scope.entities[type + num]) {
num++;
}
$scope.entities[entityType + num] = _.assign($parse(this.meta.defaults[entityType])($scope), {
$scope.entities[type + num] = _.assign($parse(meta.defaults)($scope), {
'#tag': 'af-entity',
type: entityType,
name: entityType + num,
label: entityType + ' ' + num
type: meta.entity,
name: type + num,
label: meta.label + ' ' + num
});
// Add this af-entity tag after the last existing one
var pos = 1 + _.findLastIndex($scope.layout['#children'], {'#tag': 'af-entity'});
$scope.layout['#children'].splice(pos, 0, $scope.entities[entityType + num]);
$scope.layout['#children'].splice(pos, 0, $scope.entities[type + num]);
// Create a new af-fieldset container for the entity
var fieldset = {
'#tag': 'fieldset',
'af-fieldset': entityType + num,
'af-fieldset': type + num,
'#children': [
{
'#tag': 'legend',
'class': 'af-text',
'#children': [
{
'#text': entityType + ' ' + num
'#text': meta.label + ' ' + num
}
]
}
]
};
// Add default contact name block
if (meta.entity === 'Contact') {
fieldset['#children'].push({'#tag': 'block-name-' + type.toLowerCase()});
}
// Attempt to place the new af-fieldset after the last one on the form
pos = 1 + _.findLastIndex($scope.layout['#children'], 'af-fieldset');
if (pos) {
......@@ -118,7 +122,7 @@
} else {
$scope.layout['#children'].push(fieldset);
}
return entityType + num;
return type + num;
};
this.removeEntity = function(entityName) {
......@@ -133,14 +137,14 @@
};
this.getField = function(entityType, fieldName) {
return $scope.meta.fields[entityType][fieldName];
return $scope.meta.entities[entityType].fields[fieldName];
};
this.getEntity = function(entityName) {
return $scope.entities[entityName];
};
this.getselectedEntityName = function() {
this.getSelectedEntityName = function() {
return $scope.selectedEntityName;
};
......@@ -244,10 +248,15 @@
$scope.elementList = [];
$scope.elementTitles = [];
$scope.getMeta = function() {
var type = $scope.entity.type === 'Contact' ? $scope.entity.data.contact_type : $scope.entity.type;
return $scope.editor ? $scope.editor.meta.entities[type] : {};
};
$scope.valuesFields = function() {
var fields = $scope.editor ? _.transform($scope.editor.meta.fields[$scope.entity.type], function(fields, field) {
var fields = _.transform($scope.getMeta().fields, function(fields, field) {
fields.push({id: field.name, text: field.title, disabled: $scope.fieldInUse(field.name)});
}, []) : [];
}, []);
return {results: fields};
};
......@@ -264,7 +273,7 @@
function buildFieldList(search) {
$scope.fieldList.length = 0;
_.each($scope.editor.meta.fields[$scope.entity.type], function(field) {
_.each($scope.getMeta().fields, function(field) {
if (!search || _.contains(field.name, search) || _.contains(field.title.toLowerCase(), search)) {
$scope.fieldList.push({
"#tag": "af-field",
......@@ -278,7 +287,9 @@
$scope.blockList.length = 0;
$scope.blockTitles.length = 0;
_.each($scope.editor.meta.blocks, function(block, directive) {
if (!search || _.contains(directive, search) || _.contains(block.name.toLowerCase(), search) || _.contains(block.title.toLowerCase(), search)) {
if ((!search || _.contains(directive, search) || _.contains(block.name.toLowerCase(), search) || _.contains(block.title.toLowerCase(), search)) &&
(block.block === '*' || block.block === $scope.entity.type || ($scope.entity.type === 'Contact' && block.block === $scope.entity.data.contact_type))
) {
var item = {"#tag": block.join ? "div" : directive};
if (block.join) {
item['af-join'] = block.join;
......@@ -418,7 +429,7 @@
};
$scope.isSelectedFieldset = function(entityName) {
return entityName === $scope.editor.getselectedEntityName();
return entityName === $scope.editor.getSelectedEntityName();
};
$scope.selectEntity = function() {
......
<div class="af-gui-columns">
<fieldset class="af-gui-entity-values">
<legend>{{ ts('Values:') }}</legend>
<div class="form-inline" ng-if="entity.data" ng-repeat="(fieldName, value) in entity.data">
<label>{{ editor.getField(entity.type, fieldName).title }}:</label><br />
<div class="form-inline" ng-if="getMeta().fields[fieldName]" ng-repeat="(fieldName, value) in entity.data">
<label>{{ getMeta().fields[fieldName].title }}:</label><br />
<input class="form-control" af-gui-field-value="editor.getField(entity.type, fieldName)" ng-model="entity.data[fieldName]" />
<a href class="" ng-click="removeValue(entity, fieldName)" ng-if="!editor.getField(entity.type, fieldName).required_data">
<a href ng-click="removeValue(entity, fieldName)">
<i class="crm-i fa-times"></i>
</a>
</div>
......@@ -48,7 +48,7 @@
</fieldset>
</div>
<a href ng-click="editor.removeEntity(entity.name)" class="btn btn-sm btn-danger-outline af-gui-remove-entity" title="{{ ts('Remove %1', {1: entity.type}) }}">
<a href ng-click="editor.removeEntity(entity.name)" class="btn btn-sm btn-danger-outline af-gui-remove-entity" title="{{ ts('Remove %1', {1: getMeta().label}) }}">
<i class="crm-i fa-trash"></i>
</a>
......
......@@ -15,8 +15,8 @@
<span><i class="crm-i fa-plus"></i></span>
</a>
<ul class="dropdown-menu">
<li ng-repeat="entity in meta.entities">
<a href ng-click="addEntity(entity.name)">{{ entity.name }}</a>
<li ng-repeat="(entityName, entity) in meta.entities" ng-if="entity.defaults">
<a href ng-click="addEntity(entityName)">{{ entity.label }}</a>
</li>
</ul>
</li>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment