diff --git a/Civi/Api4/Action/LocBlock/Create.php b/Civi/Api4/Action/LocBlock/Create.php
new file mode 100644
index 000000000000..5e1b17970d4b
--- /dev/null
+++ b/Civi/Api4/Action/LocBlock/Create.php
@@ -0,0 +1,21 @@
+addWhere('id', '=', $params['id'])
+ ->execute()->first();
+ }
+ foreach (['Address', 'Email', 'Phone', 'IM'] as $joinEntity) {
+ foreach (['', '_2'] as $suffix) {
+ $joinField = strtolower($joinEntity) . $suffix . '_id';
+ $item = \CRM_Utils_Array::filterByPrefix($params, "$joinField.");
+ $entityId = $params[$joinField] ?? $locBlock[$joinField] ?? NULL;
+ if ($item) {
+ $labelField = CoreUtil::getInfoItem($joinEntity, 'label_field');
+ // If NULL was given for the main field (e.g. `email`) then delete the record IF it's not in use
+ if (!empty($params['id']) && $entityId && $labelField && array_key_exists($labelField, $item) && ($item[$labelField] === NULL || $item[$labelField] === '')) {
+ $referenceCount = CoreUtil::getRefCountTotal($joinEntity, $entityId);
+ if ($referenceCount <= 1) {
+ civicrm_api4($joinEntity, 'delete', [
+ 'checkPermissions' => FALSE,
+ 'where' => [
+ ['id', '=', $entityId],
+ ],
+ ]);
+ }
+ }
+ else {
+ $item['contact_id'] = '';
+ if ($entityId) {
+ $item['id'] = $entityId;
+ }
+ $saved = civicrm_api4($joinEntity, 'save', [
+ 'checkPermissions' => FALSE,
+ 'records' => [$item],
+ ])->first();
+ $params[$joinField] = $saved['id'] ?? NULL;
+ }
+ }
+ }
+ }
+ }
+
+ protected function resolveFKValues(array &$record): void {
+ // Override parent function with noop to prevent spurious matching
+ }
+
+}
diff --git a/Civi/Api4/Action/LocBlock/Save.php b/Civi/Api4/Action/LocBlock/Save.php
new file mode 100644
index 000000000000..66b353af4e33
--- /dev/null
+++ b/Civi/Api4/Action/LocBlock/Save.php
@@ -0,0 +1,21 @@
+setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * @param bool $checkPermissions
+ * @return Action\LocBlock\Update
+ */
+ public static function update($checkPermissions = TRUE) {
+ return (new Action\LocBlock\Update('LocBlock', __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * @param bool $checkPermissions
+ * @return Action\LocBlock\Save
+ */
+ public static function save($checkPermissions = TRUE) {
+ return (new Action\LocBlock\Save('LocBlock', __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
}
diff --git a/Civi/Api4/Utils/CoreUtil.php b/Civi/Api4/Utils/CoreUtil.php
index 2d4d54f27472..baa194ad071f 100644
--- a/Civi/Api4/Utils/CoreUtil.php
+++ b/Civi/Api4/Utils/CoreUtil.php
@@ -326,6 +326,22 @@ public static function getRefCount(string $entityName, $entityId): array {
return $dao->getReferenceCounts();
}
+ /**
+ * Gets total number of references
+ *
+ * @param string $entityName
+ * @param $entityId
+ * @return int
+ * @throws NotImplementedException
+ */
+ public static function getRefCountTotal(string $entityName, $entityId): int {
+ $total = 0;
+ foreach ((array) self::getRefCount($entityName, $entityId) as $ref) {
+ $total += $ref['count'] ?? 0;
+ }
+ return $total;
+ }
+
/**
* @return array
*/
diff --git a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
index f501acf527a5..8982a5c45f28 100644
--- a/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
+++ b/ext/afform/admin/Civi/AfformAdmin/AfformAdminMeta.php
@@ -99,6 +99,13 @@ public static function getFields($entityName, $params = []) {
}
$params['values']['state_province_id'] = \Civi::settings()->get('defaultContactStateProvince');
}
+ // Exclude LocBlock fields that will be replaced by joins (see below)
+ if ($params['action'] === 'create' && $entityName === 'LocBlock') {
+ $joinParams = $params;
+ // Omit the fk fields (email_id, email_2_id, phone_id, etc)
+ // As we'll add their joined fields below
+ $params['where'][] = ['fk_entity', 'IS NULL'];
+ }
$fields = (array) civicrm_api4($entityName, 'getFields', $params);
// Add implicit joins to search fields
if ($params['action'] === 'get') {
@@ -117,6 +124,29 @@ public static function getFields($entityName, $params = []) {
}
}
}
+ // Add LocBlock joins (e.g. `email_id.email`, `address_id.street_address`)
+ if ($params['action'] === 'create' && $entityName === 'LocBlock') {
+ // Exclude fields that don't apply to locBlocks
+ $joinParams['where'][] = ['name', 'NOT IN', ['id', 'is_primary', 'is_billing', 'location_type_id', 'contact_id']];
+ foreach (['Address', 'Email', 'Phone', 'IM'] as $joinEntity) {
+ $joinEntityFields = (array) civicrm_api4($joinEntity, 'getFields', $joinParams);
+ $joinEntityLabel = CoreUtil::getInfoItem($joinEntity, 'title');
+ // LocBlock entity includes every join twice (e.g. `email_2_id.email`, `address_2_id.street_address`)
+ foreach ([1 => '', 2 => '_2'] as $number => $suffix) {
+ $joinField = strtolower($joinEntity) . $suffix . '_id';
+ foreach ($joinEntityFields as $joinEntityField) {
+ if (strtolower($joinEntity) === $joinEntityField['name']) {
+ $joinEntityField['label'] .= " $number";
+ }
+ else {
+ $joinEntityField['label'] = "$joinEntityLabel $number {$joinEntityField['label']}";
+ }
+ $joinEntityField['name'] = "$joinField." . $joinEntityField['name'];
+ $fields[] = $joinEntityField;
+ }
+ }
+ }
+ }
// Index by name
$fields = array_column($fields, NULL, 'name');
$idField = CoreUtil::getIdFieldName($entityName);
diff --git a/ext/afform/admin/afformEntities/LocBlock.php b/ext/afform/admin/afformEntities/LocBlock.php
new file mode 100644
index 000000000000..773d10b72a5d
--- /dev/null
+++ b/ext/afform/admin/afformEntities/LocBlock.php
@@ -0,0 +1,5 @@
+ 'join',
+ 'repeat_max' => 1,
+];
diff --git a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js
index 35f6617923b6..3041a1642e75 100644
--- a/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js
+++ b/ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js
@@ -108,11 +108,13 @@
}
item['af-join'] = block.join_entity;
item['#children'] = [{"#tag": directive}];
- item['af-repeat'] = ts('Add');
- item['af-copy'] = ts('Copy');
- item.min = '1';
- if (typeof joinEntity.repeat_max === 'number') {
- item.max = '' + joinEntity.repeat_max;
+ if (joinEntity.repeat_max !== 1) {
+ item['af-repeat'] = ts('Add');
+ item['af-copy'] = ts('Copy');
+ item.min = '1';
+ if (typeof joinEntity.repeat_max === 'number') {
+ item.max = '' + joinEntity.repeat_max;
+ }
}
}
$scope.blockList.push(item);
diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js
index ed782602408c..abe6df630535 100644
--- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js
+++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js
@@ -117,7 +117,7 @@
};
$scope.isRepeatable = function() {
- return ctrl.join ||
+ return (ctrl.join && $scope.getRepeatMax() !== 1) ||
(block.directive && afGui.meta.blocks[block.directive].repeat) ||
(ctrl.node['af-fieldset'] && ctrl.editor.getEntityDefn(ctrl.editor.getEntity(ctrl.node['af-fieldset'])) !== false);
};
diff --git a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html
index c0503327a242..527982757d04 100644
--- a/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html
+++ b/ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html
@@ -7,7 +7,7 @@
-
+