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 @@
- +
@@ -41,16 +52,25 @@ public function testEventTemplatePrefill(): void { 'permission' => \CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION, ]); + // Prefill from template $prefill = Afform::prefill() ->setName($this->formName) - ->setMatchField('template_id') - ->setArgs(['Event1' => [$eventTemplate['id']]]) + ->setArgs(['Event1' => [['template_id' => $eventTemplate['id']]]]) ->execute()->single(); - $this->assertSame('Test Me', $prefill['values'][0]['fields']['title']); - $this->assertSame(1, $prefill['values'][0]['fields']['event_type_id']); + $this->assertSame($eventTemplate['event_type_id'], $prefill['values'][0]['fields']['event_type_id']); $this->assertSame($eventTemplate['id'], $prefill['values'][0]['fields']['template_id']); $this->assertArrayNotHasKey('id', $prefill['values'][0]['fields']); + $this->assertSame('1@te.st', $prefill['values'][0]['joins']['LocBlock'][0]['email_id.email']); + $this->assertSame('1234567', $prefill['values'][0]['joins']['LocBlock'][0]['phone_id.phone']); + + // Prefill just the locBlock + $prefill = Afform::prefill() + ->setName($this->formName) + ->setArgs(['Event1' => [['joins' => ['LocBlock' => [['id' => $locBlock2['id']]]]]]]) + ->execute()->single(); + $this->assertSame('2@te.st', $prefill['values'][0]['joins']['LocBlock'][0]['email_id.email']); + $this->assertSame('2234567', $prefill['values'][0]['joins']['LocBlock'][0]['phone_id.phone']); } } diff --git a/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformGroupSubscriptionUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformGroupSubscriptionUsageTest.php index 57684e490ce1..5016c0c2017b 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformGroupSubscriptionUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformGroupSubscriptionUsageTest.php @@ -13,7 +13,7 @@ class AfformGroupSubscriptionUsageTest extends AfformUsageTestCase implements TransactionalInterface { /** - * Tests creating a relationship between multiple contacts + * Tests subscribing and unsubscribing to groups */ public function testGroupSubscription(): void { $groupName = __FUNCTION__; diff --git a/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformUsageTestCase.php b/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformUsageTestCase.php index 7d86d1ea70f0..378f56825a3d 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformUsageTestCase.php +++ b/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformUsageTestCase.php @@ -2,6 +2,7 @@ namespace api\v4\Afform; use Civi\Api4\Afform; +use Civi\Api4\CustomGroup; /** * Test case for Afform.prefill and Afform.submit. @@ -28,6 +29,9 @@ public function tearDown(): void { Afform::revert(FALSE) ->addWhere('name', '=', $this->formName) ->execute(); + CustomGroup::delete(FALSE) + ->addWhere('id', '>', 0) + ->execute(); parent::tearDown(); } diff --git a/tests/phpunit/api/v4/Entity/LocBlockTest.php b/tests/phpunit/api/v4/Entity/LocBlockTest.php new file mode 100644 index 000000000000..fed4245fc97e --- /dev/null +++ b/tests/phpunit/api/v4/Entity/LocBlockTest.php @@ -0,0 +1,69 @@ +addValue('email_id.email', 'first@e.mail') + ->addValue('email_2_id.email', 'second@e.mail') + ->execute()->first(); + + $locBlock = LocBlock::get(FALSE) + ->addWhere('id', '=', $locBlock1['id']) + ->addSelect('email_id', 'email_2_id', 'email_id.email', 'email_2_id.email') + ->execute()->first(); + $this->assertEquals('first@e.mail', $locBlock['email_id.email']); + $this->assertEquals('second@e.mail', $locBlock['email_2_id.email']); + $this->assertEquals($locBlock1['email_id'], $locBlock['email_id']); + $this->assertEquals($locBlock1['email_2_id'], $locBlock['email_2_id']); + + // Share an email with the 1st block + $locBlock2 = LocBlock::create(FALSE) + ->addValue('email_id', $locBlock1['email_id']) + ->addValue('email_2_id.email', 'third@e.mail') + ->execute()->first(); + + LocBlock::update(FALSE) + ->addWhere('id', '=', $locBlock1['id']) + ->addValue('email_id.email', '') + ->addValue('email_2_id.email', '') + ->execute(); + + $email1 = Email::get(FALSE) + ->addSelect('id') + ->addWhere('id', 'IN', [$locBlock1['email_id'], $locBlock1['email_2_id']]) + ->execute()->column('id'); + + $this->assertEquals([$locBlock1['email_id']], (array) $email1); + } + +} diff --git a/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php b/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php index f5ed0e2af178..9e4e1b3f54a9 100644 --- a/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php +++ b/tools/extensions/phpstorm/Civi/PhpStorm/Api4Generator.php @@ -54,6 +54,7 @@ public function generate() { $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::getTableName()', 0, 'api4Entities'); $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::getCustomGroupExtends()', 0, 'api4Entities'); $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::getRefCount()', 0, 'api4Entities'); + $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::getRefCountTotal()', 0, 'api4Entities'); $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::isType()', 0, 'api4Entities'); $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::isType()', 1, 'api4EntityTypes'); $builder->addExpectedArguments('\Civi\Api4\Utils\CoreUtil::checkAccessDelegated()', 0, 'api4Entities');