diff --git a/Civi/Api4/Action/LocBlock/LocBlockSaveTrait.php b/Civi/Api4/Action/LocBlock/LocBlockSaveTrait.php index 04359efcf738fce380189dfaa6f38238b30983b9..adc8e9dfdbcb388aa8ac7bb829fe51cd1dc77e19 100644 --- a/Civi/Api4/Action/LocBlock/LocBlockSaveTrait.php +++ b/Civi/Api4/Action/LocBlock/LocBlockSaveTrait.php @@ -48,7 +48,7 @@ trait LocBlockSaveTrait { $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 NULL was given for the required 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) { @@ -60,7 +60,8 @@ trait LocBlockSaveTrait { ]); } } - else { + // Otherwise save if the required field (e.g. `email`) has a value (or no fields are required) + elseif (!array_key_exists($labelField, $item) || (isset($item[$labelField]) && $item[$labelField] !== '')) { $item['contact_id'] = ''; if ($entityId) { $item['id'] = $entityId; diff --git a/Civi/Api4/Generic/DAOGetFieldsAction.php b/Civi/Api4/Generic/DAOGetFieldsAction.php index 9699e2cced3765c341c22427999d60abbdc517aa..90fe7c6864b2056a2903f4fec2f2f5cbb0823c15 100644 --- a/Civi/Api4/Generic/DAOGetFieldsAction.php +++ b/Civi/Api4/Generic/DAOGetFieldsAction.php @@ -43,11 +43,18 @@ class DAOGetFieldsAction extends BasicGetFieldsAction { if ($this->loadOptions) { $this->loadFieldOptions($fields, $fieldsToGet ?: array_keys($fields)); } + // Add fields across implicit FK joins foreach ($fieldsToGet ?? [] as $fieldName) { if (empty($fields[$fieldName]) && str_contains($fieldName, '.')) { $fkField = $this->getFkFieldSpec($fieldName, $fields); if ($fkField) { + $fieldPrefix = substr($fieldName, 0, 0 - strlen($fkField['name'])); $fkField['name'] = $fieldName; + // Control field should get the same prefix as it belongs to the new entity now + if (!empty($fkField['input_attrs']['control_field'])) { + $fkField['input_attrs']['control_field'] = $fieldPrefix . $fkField['input_attrs']['control_field']; + } + $fkField['required'] = FALSE; $fields[] = $fkField; } } @@ -69,7 +76,7 @@ class DAOGetFieldsAction extends BasicGetFieldsAction { * @return array|null * @throws \CRM_Core_Exception */ - private function getFkFieldSpec($fieldName, $fields) { + private function getFkFieldSpec(string $fieldName, array $fields): ?array { $fieldPath = explode('.', $fieldName); // Search for the first segment alone plus the first and second // No field in the schema contains more than one dot in its name. @@ -81,9 +88,11 @@ class DAOGetFieldsAction extends BasicGetFieldsAction { 'checkPermissions' => $this->checkPermissions, 'where' => [['name', '=', $newFieldName]], 'loadOptions' => $this->loadOptions, + 'values' => FormattingUtil::filterByPath($this->values, $fieldName, $newFieldName), 'action' => $this->action, ])->first(); } + return NULL; } /** diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Submit.php b/ext/afform/core/Civi/Api4/Action/Afform/Submit.php index 11fee493392ebbfba8e5b2f5323901834807d46e..4315e35d5e4cf0de0a44346f43bb724a24eb170f 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Submit.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Submit.php @@ -460,8 +460,8 @@ class Submit extends AbstractProcessor { // Forward FK e.g. Event.loc_block_id => LocBlock $forwardFkField = self::getFkField($mainEntity['type'], $joinEntityName); if ($forwardFkField && $values) { - // Add id to values for update op - if ($whereClause) { + // Add id to values for update op, but only if id is not already on the form + if ($whereClause && empty($mainEntity['joins'][$joinEntityName]['fields'][$joinIdField])) { $values[0][$joinIdField] = $whereClause[0][2]; } $result = civicrm_api4($joinEntityName, 'save', [ diff --git a/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformEventUsageTest.php b/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformEventUsageTest.php index 75149a9ef6e5b816d1275aece3a335350618a40e..225fa30552985efb1749fbf81d484b5971d5b84b 100644 --- a/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformEventUsageTest.php +++ b/ext/afform/mock/tests/phpunit/api/v4/Afform/AfformEventUsageTest.php @@ -2,13 +2,14 @@ namespace api\v4\Afform; use Civi\Api4\Afform; +use Civi\Test\TransactionalInterface; /** * Test case for Afform.prefill and Afform.submit with Event records. * * @group headless */ -class AfformEventUsageTest extends AfformUsageTestCase { +class AfformEventUsageTest extends AfformUsageTestCase implements TransactionalInterface { use \Civi\Test\Api4TestTrait; /** @@ -75,4 +76,110 @@ EOHTML; $this->assertSame('2234567', $prefill['values'][0]['joins']['LocBlock'][0]['phone_id.phone']); } + /** + * Test saving & updating + */ + public function testEventLocationUpdate(): void { + $layout = <<<EOHTML +<af-form ctrl="afform"> + <af-entity actions="{create: true, update: true}" type="Event" name="Event1" label="Event 1" security="FBAC" /> + <fieldset af-fieldset="Event1" class="af-container" af-title="Event 1"> + <af-field name="id" /> + <af-field name="title" /> + <af-field name="start_date" /> + <af-field name="event_type_id" /> + <div af-join="LocBlock"> + <af-field name="id" /> + <af-field name="email_id.email" /> + <af-field name="address_id.street_address" /> + </div> + </fieldset> + <button class="af-button btn btn-primary" crm-icon="fa-check" ng-click="afform.submit()">Submit</button> +</af-form> +EOHTML; + + $this->useValues([ + 'layout' => $layout, + 'permission' => \CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION, + ]); + + // Create a new event with a new location + $submit = Afform::submit() + ->setName($this->formName) + ->setValues([ + 'Event1' => [ + [ + 'fields' => [ + 'title' => 'Event Title 1', + 'start_date' => '2021-01-01 00:00:00', + 'event_type_id' => 1, + ], + 'joins' => [ + 'LocBlock' => [ + [ + 'email_id.email' => 'test1@te.st', + 'address_id.street_address' => '12345', + ], + ], + ], + ], + ], + ])->execute(); + + $event1 = $submit[0]['Event1'][0]['id']; + $loc1 = $submit[0]['Event1'][0]['joins']['LocBlock'][0]['id']; + + // Create a 2nd new event with a new location + $submit = Afform::submit() + ->setName($this->formName) + ->setValues([ + 'Event1' => [ + [ + 'fields' => [ + 'title' => 'Event Title 2', + 'start_date' => '2022-01-01 00:00:00', + 'event_type_id' => 1, + ], + 'joins' => [ + 'LocBlock' => [ + [ + 'id' => $loc1, + ], + ], + ], + ], + ], + ])->execute(); + + $event2 = $submit[0]['Event1'][0]['id']; + $this->assertGreaterThan($event1, $event2); + $this->assertSame($loc1, $submit[0]['Event1'][0]['joins']['LocBlock'][0]['id']); + + // Update event 1 with a new location + $submit = Afform::submit() + ->setName($this->formName) + ->setValues([ + 'Event1' => [ + [ + 'fields' => [ + 'id' => $event1, + 'title' => 'Event Title 1 Updated', + ], + 'joins' => [ + 'LocBlock' => [ + [ + 'id' => NULL, + 'address_id.street_address' => '12345', + ], + ], + ], + ], + ], + ])->execute(); + + $this->assertSame($event1, $submit[0]['Event1'][0]['id']); + $this->assertGreaterThan($loc1, $submit[0]['Event1'][0]['joins']['LocBlock'][0]['id']); + + } + } diff --git a/tests/phpunit/api/v4/Action/GetExtraFieldsTest.php b/tests/phpunit/api/v4/Action/GetExtraFieldsTest.php index 27f56c525c4c6a02bf8324d43ffad40ceffc9327..01798f9bb981fc8eb0fef4b3be737753a3513067 100644 --- a/tests/phpunit/api/v4/Action/GetExtraFieldsTest.php +++ b/tests/phpunit/api/v4/Action/GetExtraFieldsTest.php @@ -25,6 +25,7 @@ use Civi\Api4\Address; use Civi\Api4\Contact; use Civi\Api4\Household; use Civi\Api4\Individual; +use Civi\Api4\LocBlock; use Civi\Api4\Tag; /** @@ -101,6 +102,20 @@ class GetExtraFieldsTest extends Api4TestBase { $this->assertGreaterThan(1, count($fields['contact_id.gender_id']['options'])); } + public function testGetLocBlockFields() { + $field = LocBlock::getFields(FALSE) + ->setLoadOptions(TRUE) + ->addWhere('name', '=', 'address_id.state_province_id') + ->addValue('address_id.country_id', 1039) + ->execute()->single(); + + $this->assertEquals('Address', $field['entity']); + $this->assertEquals('address_id.state_province_id', $field['name']); + $this->assertEquals('address_id.country_id', $field['input_attrs']['control_field']); + $this->assertContains('Manitoba', $field['options']); + $this->assertNotContains('Alabama', $field['options']); + } + public function testGetTagsFromFilterField(): void { $actTag = Tag::create(FALSE) ->addValue('name', uniqid('act')) diff --git a/tests/phpunit/api/v4/Entity/LocBlockTest.php b/tests/phpunit/api/v4/Entity/LocBlockTest.php index fed4245fc97eb5c5e4c6db780847e54be294c09e..b4e8890327634cf6b6bf77d228739d216b3ebd2f 100644 --- a/tests/phpunit/api/v4/Entity/LocBlockTest.php +++ b/tests/phpunit/api/v4/Entity/LocBlockTest.php @@ -22,11 +22,12 @@ namespace api\v4\Entity; use api\v4\Api4TestBase; use Civi\Api4\Email; use Civi\Api4\LocBlock; +use Civi\Test\TransactionalInterface; /** * @group headless */ -class LocBlockTest extends Api4TestBase { +class LocBlockTest extends Api4TestBase implements TransactionalInterface { /** * @throws \CRM_Core_Exception @@ -52,18 +53,54 @@ class LocBlockTest extends Api4TestBase { ->addValue('email_2_id.email', 'third@e.mail') ->execute()->first(); + // Void both emails in block 1 LocBlock::update(FALSE) ->addWhere('id', '=', $locBlock1['id']) ->addValue('email_id.email', '') ->addValue('email_2_id.email', '') ->execute(); + // 1 email has been deleted, the other preserved (because it's shared with block 2) $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); + $this->assertEquals([$locBlock1['email_id']], $email1); + } + + public function testLocBlockWithBlankValues(): void { + $locBlockId = LocBlock::create(FALSE) + ->addValue('address_id.street_address', '') + ->addValue('phone_id.phone', '') + ->addValue('email_id.email', '') + ->execute()->first()['id']; + + // Get locBlock + $locBlock = LocBlock::get(FALSE) + ->addWhere('id', '=', $locBlockId) + ->execute()->single(); + + $this->assertNotEmpty($locBlock['address_id']); + $this->assertEmpty($locBlock['phone_id']); + $this->assertEmpty($locBlock['email_id']); + + // Update with a 2nd blank email & an address + LocBlock::update(FALSE) + ->addWhere('id', '=', $locBlockId) + ->addValue('email2_id.email', '') + ->addValue('address_id.street_address', '123') + ->execute(); + + $updatedLocBlock = LocBlock::get(FALSE) + ->addWhere('id', '=', $locBlockId) + ->addSelect('*', 'address_id.street_address', 'phone_id.phone', 'email_id.email') + ->execute()->single(); + + $this->assertEquals($locBlock['address_id'], $updatedLocBlock['address_id']); + $this->assertEquals('123', $updatedLocBlock['address_id.street_address']); + $this->assertNull($updatedLocBlock['phone_id.phone']); + $this->assertNull($updatedLocBlock['email_id.email']); } }