Skip to content

Commit

Permalink
Afform - Refactor prefill to use arrays keyed by matchField instead o…
Browse files Browse the repository at this point in the history
…f a matchField param
  • Loading branch information
colemanw committed Aug 9, 2024
1 parent 95975f4 commit 7fdff5c
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 100 deletions.
1 change: 1 addition & 0 deletions Civi/Api4/LocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Links addresses, emails & phones to Events.
*
* @searchable secondary
* @searchFields address_id.street_address,address_id.city,email_id.email
* @since 5.31
* @package Civi\Api4
*/
Expand Down
10 changes: 7 additions & 3 deletions ext/afform/core/Civi/Afform/Behavior/ContactAutofill.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public static function onAfformPrefill(AfformPrefillEvent $event): void {
if (!$id && $autoFillMode === 'user' && !$event->getApiRequest()->getArgs()) {
$id = \CRM_Core_Session::getLoggedInContactID();
if ($id) {
$event->getApiRequest()->loadEntity($entity, [$id]);
$event->getApiRequest()->loadEntity($entity, [['id' => $id]]);
}
}
// Autofill by relationship
Expand All @@ -123,8 +123,12 @@ public static function onAfformPrefill(AfformPrefillEvent $event): void {
->addWhere('far_contact_id', '=', $relatedContact)
->addWhere('near_contact_id.is_deleted', '=', FALSE)
->addWhere('is_current', '=', TRUE)
->execute()->column('near_contact_id');
$event->getApiRequest()->loadEntity($entity, $relations);
->execute();
$relatedIds = [];
foreach ($relations as $relation) {
$relatedIds[] = ['id' => $relation['near_contact_id']];
}
$event->getApiRequest()->loadEntity($entity, $relatedIds);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ext/afform/core/Civi/Afform/Behavior/GroupSubscription.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static function onAfformPrefill(AfformPrefillEvent $event): void {
$cid = $event->getEntityIds($contact)[0] ?? NULL;
}
if ($cid) {
$event->getApiRequest()->loadEntity($subscriptionEntity, [$cid]);
$event->getApiRequest()->loadEntity($subscriptionEntity, [['contact_id' => $cid]]);
}
}

Expand Down
131 changes: 91 additions & 40 deletions ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,27 @@ protected function loadEntities() {
}

foreach ($sortedEntities as $entityName) {
$ids = (array) ($this->args[$entityName] ?? []);

$entity = $this->_formDataModel->getEntity($entityName);
$this->_entityIds[$entityName] = [];
$matchField = $this->matchField ?? CoreUtil::getIdFieldName($entity['type']);
$matchFieldDefn = $this->_formDataModel->getField($entity['type'], $matchField, 'create');
if (!empty($entity['actions'][$matchFieldDefn['input_attrs']['autofill']])) {
if (
!empty($this->args[$entityName]) &&
(!empty($entity['url-autofill']) || isset($entity['fields'][$matchField]))
) {
$ids = (array) $this->args[$entityName];
$this->loadEntity($entity, $ids);
$idField = CoreUtil::getIdFieldName($entity['type']);

foreach ($ids as $num => $id) {
// Url args may be scalar - convert to array format
if (is_scalar($id)) {
$ids[$num] = [$idField => $id];
}
}
if ($ids) {
// If 'update' (or 'create' in special cases like 'template_id') is allowed, load entity.
$matchField = self::getNestedKey($ids) ?: $idField;
$matchFieldDefn = $this->_formDataModel->getField($entity['type'], $matchField, 'create');
$autofillMode = $matchFieldDefn['input_attrs']['autofill'] ?? NULL;
if (!empty($entity['actions'][$autofillMode])) {
if (!empty($entity['url-autofill']) || isset($entity['fields'][$matchField])) {
$this->loadEntity($entity, $ids, $autofillMode);
}
}
}
$event = new AfformPrefillEvent($this->_afform, $this->_formDataModel, $this, $entity['type'], $entityName, $this->_entityIds);
Expand Down Expand Up @@ -143,37 +153,65 @@ protected function prePopulateSubmissionData($sortedEntities) {
* Fetch all data needed to display a given entity on this form
*
* @param array $entity
* @param array $ids
* Afform entity definition
* @param array[] $values
* Array of value arrays. Each must be the primary key, e.g.
* ```
* [
* ['id' => 123],
* ['id' => 456],
* ]
* ```
* In theory we could include other stuff in the values, but it's not currently supported.
* @param string $mode
* 'update' or 'create' ('create' is only used in special cases like `Event.template_id`)
*/
public function loadEntity(array $entity, array $ids) {
public function loadEntity(array $entity, array $values, string $mode = 'update'): void {
// Limit number of records based on af-repeat settings
// If 'min' is set then it is repeatable, and max will either be a number or NULL for unlimited.
if (isset($entity['min']) && isset($entity['max'])) {
foreach (array_keys($ids) as $index) {
if ($index >= $entity['max']) {
unset($ids[$index]);
foreach (array_keys($values) as $count => $index) {
if ($count >= $entity['max']) {
unset($values[$index]);
}
}
}
$matchField = self::getNestedKey($values);
if (!$matchField) {
return;
}
$keys = array_combine(array_keys($values), array_column($values, $matchField));
// In create mode, use id as the key
$keyField = $mode === 'create' ? CoreUtil::getIdFieldName($entity['name']) : $matchField;

$api4 = $this->_formDataModel->getSecureApi4($entity['name']);
$idField = CoreUtil::getIdFieldName($entity['type']);
if ($ids && !empty($entity['fields'][$idField]['defn']['saved_search'])) {
$ids = $this->validateBySavedSearch($entity, $ids);
if ($keys && !empty($entity['fields'][$keyField]['defn']['saved_search'])) {
$keys = $this->validateBySavedSearch($entity, $keys, $matchField);
}
if (!$ids) {
if (!$keys) {
return;
}
$result = $this->apiGet($api4, $entity['type'], $entity['fields'], [
'where' => [[$idField, 'IN', $ids]],
$result = $this->apiGet($api4, $entity['type'], $entity['fields'], $keyField, [
'where' => [[$keyField, 'IN', $keys]],
]);
foreach ($ids as $index => $id) {
$this->_entityIds[$entity['name']][$index] = [
$idField => isset($result[$id]) ? $id : NULL,
'joins' => [],
];
if (isset($result[$id])) {
$data = ['fields' => $result[$id]];
$idField = CoreUtil::getIdFieldName($entity['type']);
foreach ($keys as $index => $key) {
$entityId = $result[$key][$idField] ?? NULL;
// In create mode, swap id with matchField
if ($mode === 'create') {
if (isset($result[$key][$idField])) {
$result[$key][$matchField] = $result[$key][$idField];
unset($result[$key][$idField]);
}
}
else {
$this->_entityIds[$entity['name']][$index] = [
$idField => $entityId,
'joins' => [],
];
}
if (!empty($result[$key])) {
$data = ['fields' => $result[$key]];
foreach ($entity['joins'] ?? [] as $joinEntity => $join) {
$joinIdField = CoreUtil::getIdFieldName($joinEntity);
$multipleLocationBlocks = is_array($join['data']['location_type_id'] ?? NULL);
Expand All @@ -186,9 +224,9 @@ public function loadEntity(array $entity, array $ids) {
if ($multipleLocationBlocks) {
$limit = 0;
}
$where = self::getJoinWhereClause($this->_formDataModel, $entity['name'], $joinEntity, $id);
$where = self::getJoinWhereClause($this->_formDataModel, $entity['name'], $joinEntity, $entityId);
if ($where) {
$joinResult = $this->apiGet($api4, $joinEntity, $join['fields'] + ($join['data'] ?? []), [
$joinResult = $this->apiGet($api4, $joinEntity, $join['fields'] + ($join['data'] ?? []), $joinIdField, [
'where' => $where,
'limit' => $limit,
'orderBy' => self::getEntityField($joinEntity, 'is_primary') ? ['is_primary' => 'DESC'] : [],
Expand Down Expand Up @@ -216,16 +254,18 @@ public function loadEntity(array $entity, array $ids) {
/**
* Delegated by loadEntity to call API.get and fill in additioal info
*
* @param $api4
* @param $entityName
* @param $entityFields
* @param $params
* @param callable $api4
* @param string $entityName
* @param array $entityFields
* @param string $keyField
* @param array $params
* @return array
*/
private function apiGet($api4, $entityName, $entityFields, $params) {
private function apiGet($api4, $entityName, $entityFields, string $keyField, $params) {
$idField = CoreUtil::getIdFieldName($entityName);
// Ensure 'id' is selected
$params['select'] = array_unique(array_merge([$idField], array_keys($entityFields)));
$result = (array) $api4($entityName, 'get', $params)->indexBy($idField);
$result = (array) $api4($entityName, 'get', $params)->indexBy($keyField);
// Fill additional info about file fields
$fileFields = $this->getFileFields($entityName, $entityFields);
foreach ($fileFields as $fieldName => $fieldDefn) {
Expand Down Expand Up @@ -257,18 +297,18 @@ protected static function getFileFields($entityName, $entityFields): array {
/**
* Validate that given id(s) are actually returned by the Autocomplete API
*
* @param $entity
* @param array $entity
* @param array $ids
* @param string $matchField
* @return array
* @throws \CRM_Core_Exception
*/
private function validateBySavedSearch($entity, array $ids) {
$idField = CoreUtil::getIdFieldName($entity['type']);
private function validateBySavedSearch(array $entity, array $ids, string $matchField) {
$fetched = civicrm_api4($entity['type'], 'autocomplete', [
'ids' => $ids,
'formName' => 'afform:' . $this->name,
'fieldName' => $entity['name'] . ':' . $idField,
])->indexBy($idField);
'fieldName' => $entity['name'] . ':' . $matchField,
])->indexBy($matchField);
$validIds = [];
// Preserve keys
foreach ($ids as $index => $id) {
Expand Down Expand Up @@ -559,4 +599,15 @@ public function processFormData(array $entityValues) {
}
}

/**
* Given a nested array like `[0 => ['id' => 123]]`,
* this returns the first key from the inner array, e.g. `'id'`.
* @param array $values
* @return int|string|null
*/
protected static function getNestedKey(array $values) {
$firstValue = \CRM_Utils_Array::first(array_filter($values));
return is_array($firstValue) && $firstValue ? array_keys($firstValue)[0] : NULL;
}

}
34 changes: 0 additions & 34 deletions ext/afform/core/Civi/Api4/Action/Afform/Prefill.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,22 @@

namespace Civi\Api4\Action\Afform;

use Civi\Api4\Utils\CoreUtil;

/**
* Class Prefill
*
* @method $this setMatchField(bool $matchField)
* @method bool getMatchField()
* @package Civi\Api4\Action\Afform
*/
class Prefill extends AbstractProcessor {

/**
* Name of the field being matched (typically 'id')
* @var string
*/
protected $matchField;

protected function processForm() {
$entityValues = $this->_entityValues;
foreach ($entityValues as $afformEntityName => &$valueSets) {
$afformEntity = $this->_formDataModel->getEntity($afformEntityName);
if ($this->matchField) {
$this->handleMatchField($afformEntity['type'], $valueSets);
}
$this->formatViewValues($afformEntity, $valueSets);
}
return \CRM_Utils_Array::makeNonAssociative($entityValues, 'name', 'values');
}

/**
* Set entity values based on an existing record.
*
* This is used for e.g. Event.template_id,
* based on 'autofill' => 'create' metadata in APIv4 getFields.
*
* @param string $entityType
* @param array $valueSets
*/
private function handleMatchField(string $entityType, array &$valueSets): void {
$matchFieldDefn = $this->_formDataModel->getField($entityType, $this->matchField, 'create');
// @see EventCreationSpecProvider for the `template_id` declaration which includes this 'autofill' = 'create' flag.
if (($matchFieldDefn['input_attrs']['autofill'] ?? NULL) === 'create') {
$idField = CoreUtil::getIdFieldName($entityType);
foreach ($valueSets as &$valueSet) {
$valueSet['fields'][$this->matchField] = $valueSet['fields'][$idField];
unset($valueSet['fields'][$idField]);
}
}
}

private function formatViewValues(array $afformEntity, array &$valueSets): void {
$originalValues = $valueSets;
foreach ($this->getDisplayOnlyFields($afformEntity['fields']) as $fieldName) {
Expand Down
10 changes: 6 additions & 4 deletions ext/afform/core/ang/af/afField.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,12 @@
// ngChange callback from Existing entity field
ctrl.onSelectEntity = function() {
if (ctrl.defn.input_attrs && ctrl.defn.input_attrs.autofill) {
var val = $scope.getSetSelect();
var entity = ctrl.afFieldset.modelName;
var index = ctrl.getEntityIndex();
ctrl.afFieldset.afFormCtrl.loadData(entity, index, val, ctrl.defn.name);
const val = $scope.getSetSelect();
const entity = ctrl.afFieldset.modelName;
const entityIndex = ctrl.getEntityIndex();
const joinEntity = ctrl.afJoin ? ctrl.afJoin.entity : null;
const joinIndex = ctrl.afJoin && $scope.dataProvider.repeatIndex || null;
ctrl.afFieldset.afFormCtrl.loadData(entity, entityIndex, val, ctrl.defn.name, joinEntity, joinIndex);
}
};

Expand Down
12 changes: 9 additions & 3 deletions ext/afform/core/ang/af/afForm.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,21 @@
// With no arguments this will prefill the entire form based on url args
// and also check if the form is open for submissions.
// With selectedEntity, selectedIndex & selectedId provided this will prefill a single entity
this.loadData = function(selectedEntity, selectedIndex, selectedId, selectedField) {
this.loadData = function(selectedEntity, selectedIndex, selectedId, selectedField, joinEntity, joinIndex) {
let toLoad = true;
const params = {name: ctrl.getFormMeta().name, args: {}};
// Load single entity
if (selectedEntity) {
toLoad = !!selectedId;
params.matchField = selectedField;
params.args[selectedEntity] = {};
params.args[selectedEntity][selectedIndex] = selectedId;
params.args[selectedEntity][selectedIndex] = {};
if (joinEntity) {
params.args[selectedEntity][selectedIndex].joins = {joinEntity: {}};
params.args[selectedEntity][selectedIndex].joins.joinEntity[joinIndex] = {};
params.args[selectedEntity][selectedIndex].joins.joinEntity[joinIndex][selectedField] = selectedId;
} else {
params.args[selectedEntity][selectedIndex][selectedField] = selectedId;
}
}
// Prefill entire form
else {
Expand Down
1 change: 1 addition & 0 deletions ext/afform/core/ang/afblockEventLocBlock.aff.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<af-field name="id" />
<div class="af-container">
<af-field name="email_id.email" />
<af-field name="phone_id.phone" />
Expand Down
Loading

0 comments on commit 7fdff5c

Please sign in to comment.