diff --git a/Civi/Api4/Generic/BasicGetFieldsAction.php b/Civi/Api4/Generic/BasicGetFieldsAction.php index 7bb18411efc4..483cf7353e7d 100644 --- a/Civi/Api4/Generic/BasicGetFieldsAction.php +++ b/Civi/Api4/Generic/BasicGetFieldsAction.php @@ -282,9 +282,16 @@ public function fields() { ], [ 'name' => 'required', + 'description' => 'Is this field required when creating a new entity', 'data_type' => 'Boolean', 'default_value' => FALSE, ], + [ + 'name' => 'nullable', + 'description' => 'Whether a null value is allowed in this field', + 'data_type' => 'Boolean', + 'default_value' => TRUE, + ], [ 'name' => 'required_if', 'data_type' => 'String', diff --git a/Civi/Api4/Service/Spec/FieldSpec.php b/Civi/Api4/Service/Spec/FieldSpec.php index e1da6ce9c15f..89f13864160f 100644 --- a/Civi/Api4/Service/Spec/FieldSpec.php +++ b/Civi/Api4/Service/Spec/FieldSpec.php @@ -63,6 +63,11 @@ class FieldSpec { /** * @var bool */ + public $nullable = TRUE; + + /** + * @var string + */ public $requiredIf; /** @@ -127,6 +132,24 @@ public function getEntity() { return $this->entity; } + /** + * @return bool + */ + public function getNullable() { + return $this->nullable; + } + + /** + * @param bool $nullable + * + * @return $this + */ + public function setNullable(bool $nullable) { + $this->nullable = $nullable; + + return $this; + } + /** * @return bool */ @@ -146,14 +169,14 @@ public function setRequired($required) { } /** - * @return bool + * @return string */ public function getRequiredIf() { return $this->requiredIf; } /** - * @param bool $requiredIf + * @param string $requiredIf * * @return $this */ diff --git a/Civi/Api4/Service/Spec/SpecFormatter.php b/Civi/Api4/Service/Spec/SpecFormatter.php index cee41e1ac1da..058f83dea24f 100644 --- a/Civi/Api4/Service/Spec/SpecFormatter.php +++ b/Civi/Api4/Service/Spec/SpecFormatter.php @@ -37,6 +37,7 @@ public static function arrayToField(array $data, $entity) { $field->setTableName($data['custom_group_id.table_name']); } $field->setColumnName($data['column_name']); + $field->setNullable(empty($data['is_required'])); $field->setCustomFieldId($data['id'] ?? NULL); $field->setCustomGroupName($data['custom_group_id.name']); $field->setTitle($data['label']); @@ -58,7 +59,8 @@ public static function arrayToField(array $data, $entity) { $field = new FieldSpec($name, $entity, $dataTypeName); $field->setType('Field'); $field->setColumnName($name); - $field->setRequired(!empty($data['required'])); + $field->setNullable(empty($data['required'])); + $field->setRequired(!empty($data['required']) && empty($data['default'])); $field->setTitle($data['title'] ?? NULL); $field->setLabel($data['html']['label'] ?? NULL); if (!empty($data['pseudoconstant'])) { diff --git a/Civi/Api4/Service/Spec/SpecGatherer.php b/Civi/Api4/Service/Spec/SpecGatherer.php index c4f49b7c9b53..d166207bda1b 100644 --- a/Civi/Api4/Service/Spec/SpecGatherer.php +++ b/Civi/Api4/Service/Spec/SpecGatherer.php @@ -97,9 +97,6 @@ private function addDAOFields($entity, $action, RequestSpec $spec) { ) { continue; } - if ($action !== 'create' || isset($DAOField['default'])) { - $DAOField['required'] = FALSE; - } if ($DAOField['name'] == 'is_active' && empty($DAOField['default'])) { $DAOField['default'] = '1'; } diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index 8f5a98c72b03..28c92d7e16af 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -497,7 +497,7 @@ private function formatEditableColumn($column, $data) { /** * @param $key - * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string}|null + * @return array{entity: string, input_type: string, data_type: string, options: bool, serialize: bool, nullable: bool, fk_entity: string, value_key: string, value_path: string, id_key: string, id_path: string}|null */ private function getEditableInfo($key) { [$key] = explode(':', $key); @@ -520,6 +520,7 @@ private function getEditableInfo($key) { 'data_type' => $field['data_type'], 'options' => !empty($field['options']), 'serialize' => !empty($field['serialize']), + 'nullable' => !empty($field['nullable']), 'fk_entity' => $field['fk_entity'], 'value_key' => $field['name'], 'value_path' => $key, diff --git a/ext/search_kit/Civi/Search/Admin.php b/ext/search_kit/Civi/Search/Admin.php index f811dd772fca..21f87cfff69b 100644 --- a/ext/search_kit/Civi/Search/Admin.php +++ b/ext/search_kit/Civi/Search/Admin.php @@ -136,7 +136,7 @@ public static function getSchema(): array { $entity['links'] = array_values($links); } $getFields = civicrm_api4($entity['name'], 'getFields', [ - 'select' => ['name', 'title', 'label', 'description', 'type', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly', 'operators'], + 'select' => ['name', 'title', 'label', 'description', 'type', 'options', 'input_type', 'input_attrs', 'data_type', 'serialize', 'entity', 'fk_entity', 'readonly', 'operators', 'nullable'], 'where' => [['name', 'NOT IN', ['api_key', 'hash']]], 'orderBy' => ['label'], ]); diff --git a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js index 052b9dcfbc5f..f205e4a3aa05 100644 --- a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js +++ b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.component.js @@ -29,6 +29,7 @@ options: col.edit.options, fk_entity: col.edit.fk_entity, serialize: col.edit.serialize, + nullable: col.edit.nullable }; $(document).on('keydown.crmSearchDisplayEditable', function(e) { @@ -36,8 +37,6 @@ $scope.$apply(function() { ctrl.cancel(); }); - } else if (e.key === 'Enter') { - $scope.$apply(ctrl.save); } }); @@ -71,7 +70,7 @@ action: 'update', select: ['options'], loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'], - where: [['name', '=', ctrl.field.name]], + where: [['name', '=', ctrl.field.name]] }, 0).then(function(field) { ctrl.field.options = optionsCache[cacheKey] = field.options; }); diff --git a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.html b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.html index d0d8eca08f3c..767c64229470 100644 --- a/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.html +++ b/ext/search_kit/ang/crmSearchDisplay/crmSearchDisplayEditable.html @@ -1,9 +1,11 @@ - -
- - -
+
+ +
+ + +
+
diff --git a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/date.html b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/date.html index 78e4d03d6d66..29d89573e58a 100644 --- a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/date.html +++ b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/date.html @@ -10,8 +10,8 @@
- - + +
diff --git a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/float.html b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/float.html index 89b0079a789e..f34a1184a9f0 100644 --- a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/float.html +++ b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/float.html @@ -1,6 +1,6 @@
- +
- +
diff --git a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/integer.html b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/integer.html index 0c5e75c49abf..3f480c2b14e2 100644 --- a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/integer.html +++ b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/integer.html @@ -1,6 +1,6 @@
- +
- +
diff --git a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/select.html b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/select.html index 464c92e43171..b46e8014b31b 100644 --- a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/select.html +++ b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/select.html @@ -2,7 +2,7 @@
- +
diff --git a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/text.html b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/text.html index 61a4391c48d5..3718a11da75a 100644 --- a/ext/search_kit/ang/crmSearchTasks/crmSearchInput/text.html +++ b/ext/search_kit/ang/crmSearchTasks/crmSearchInput/text.html @@ -1,6 +1,6 @@
- +
- +
diff --git a/ext/search_kit/ang/crmSearchTasks/crmSearchTaskUpdate.ctrl.js b/ext/search_kit/ang/crmSearchTasks/crmSearchTaskUpdate.ctrl.js index 5fd14a504c39..01f41d16fd38 100644 --- a/ext/search_kit/ang/crmSearchTasks/crmSearchTaskUpdate.ctrl.js +++ b/ext/search_kit/ang/crmSearchTasks/crmSearchTaskUpdate.ctrl.js @@ -13,7 +13,7 @@ crmApi4(this.entity, 'getFields', { action: 'update', - select: ['name', 'label', 'description', 'input_type', 'data_type', 'serialize', 'options', 'fk_entity'], + select: ['name', 'label', 'description', 'input_type', 'data_type', 'serialize', 'options', 'fk_entity', 'nullable'], loadOptions: ['id', 'name', 'label', 'description', 'color', 'icon'], where: [["readonly", "=", false]], }).then(function(fields) { diff --git a/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php b/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php index 4cc32bbcf010..8c8c4c7df104 100644 --- a/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php +++ b/tests/phpunit/api/v4/Action/BasicCustomFieldTest.php @@ -130,12 +130,22 @@ public function testWithTwoFields() { ->addValue('label', 'FavFood') ->addValue('custom_group_id', '$id') ->addValue('html_type', 'Text') + ->addValue('is_required', TRUE) ->addValue('data_type', 'String')) ->execute(); // Test that no new option groups have been created (these are text fields with no options) $this->assertEquals($optionGroupCount, OptionGroup::get(FALSE)->selectRowCount()->execute()->count()); + // Check getFields output + $fields = Contact::getFields(FALSE)->execute()->indexBy('name'); + $this->assertFalse($fields['MyContactFields2.FavColor']['required']); + $this->assertTRUE($fields['MyContactFields2.FavColor']['nullable']); + // Custom fields are never actually *required* in the api, even if is_required = true + $this->assertFalse($fields['MyContactFields2.FavFood']['required']); + // But the api will report is_required as not nullable + $this->assertFalse($fields['MyContactFields2.FavFood']['nullable']); + $contactId1 = Contact::create(FALSE) ->addValue('first_name', 'Johann') ->addValue('last_name', 'Tester') diff --git a/tests/phpunit/api/v4/Action/GetFieldsTest.php b/tests/phpunit/api/v4/Action/GetFieldsTest.php index d5833dd9e788..e58f06652033 100644 --- a/tests/phpunit/api/v4/Action/GetFieldsTest.php +++ b/tests/phpunit/api/v4/Action/GetFieldsTest.php @@ -20,6 +20,7 @@ namespace api\v4\Action; use api\v4\UnitTestCase; +use Civi\Api4\Activity; use Civi\Api4\Campaign; use Civi\Api4\Contact; use Civi\Api4\Contribution; @@ -93,4 +94,15 @@ public function testPreloadFalse() { $this->assertEquals(['name', 'label'], $fields['campaign_id']['suffixes']); } + public function testRequiredAndNullable() { + $actFields = Activity::getFields(FALSE) + ->setAction('create') + ->execute()->indexBy('name'); + + $this->assertTrue($actFields['activity_type_id']['required']); + $this->assertFalse($actFields['activity_type_id']['nullable']); + $this->assertFalse($actFields['subject']['required']); + $this->assertTrue($actFields['subject']['nullable']); + } + }