* @param int $entityId
* @param \Civi\Api4\Generic\Result $result
* @param string $parentName
+ * @param array $excludeFields
*/
- private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL) {
+ private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL, $excludeFields = []) {
if (isset($this->exportedEntities[$entityType][$entityId])) {
throw new \API_Exception("Circular reference detected: attempted to export $entityType id $entityId multiple times.");
}
$this->exportedEntities[$entityType][$entityId] = TRUE;
$select = $pseudofields = [];
- $allFields = $this->getFieldsForExport($entityType, TRUE);
+ $allFields = $this->getFieldsForExport($entityType, TRUE, $excludeFields);
foreach ($allFields as $field) {
// Use implicit join syntax but only if the fk entity has a `name` field
if (!empty($field['fk_entity']) && array_key_exists('name', $this->getFieldsForExport($field['fk_entity']))) {
/** @var \CRM_Core_DAO $dao */
$dao = new $daoName();
$dao->id = $entityId;
+ // Collect references into arrays keyed by entity type
+ $references = [];
foreach ($dao->findReferences() as $reference) {
- $refEntity = $reference::fields()['id']['entity'] ?? '';
+ $refEntity = \CRM_Utils_Array::first($reference::fields())['entity'] ?? '';
+ $references[$refEntity][] = $reference;
+ }
+ foreach ($references as $refEntity => $records) {
$refApiType = CoreUtil::getInfoItem($refEntity, 'type') ?? [];
// Reference must be a ManagedEntity
- if (in_array('ManagedEntity', $refApiType, TRUE)) {
- $this->exportRecord($refEntity, $reference->id, $result, $name . '_');
+ if (!in_array('ManagedEntity', $refApiType, TRUE)) {
+ continue;
+ }
+ $exclude = [];
+ // For sortable entities, order by weight and exclude weight from the export (it will be auto-managed)
+ if (in_array('SortableEntity', $refApiType, TRUE)) {
+ $exclude[] = $weightCol = CoreUtil::getInfoItem($refEntity, 'order_by');
+ usort($records, function($a, $b) use ($weightCol) {
+ if (!isset($a->$weightCol)) {
+ $a->find(TRUE);
+ }
+ if (!isset($b->$weightCol)) {
+ $b->find(TRUE);
+ }
+ return $a->$weightCol < $b->$weightCol ? -1 : 1;
+ });
+ }
+ foreach ($records as $record) {
+ $this->exportRecord($refEntity, $record->id, $result, $name . '_', $exclude);
}
}
}
/**
* @param $entityType
* @param bool $loadOptions
+ * @param array $excludeFields
* @return array
*/
- private function getFieldsForExport($entityType, $loadOptions = FALSE): array {
+ private function getFieldsForExport($entityType, $loadOptions = FALSE, $excludeFields = []): array {
+ $conditions = [
+ ['type', 'IN', ['Field', 'Custom']],
+ ['readonly', '!=', TRUE],
+ ];
+ if ($excludeFields) {
+ $conditions[] = ['name', 'NOT IN', $excludeFields];
+ }
try {
return (array) civicrm_api4($entityType, 'getFields', [
'action' => 'create',
- 'where' => [
- ['type', 'IN', ['Field', 'Custom']],
- ['readonly', '!=', TRUE],
- ],
+ 'where' => $conditions,
'loadOptions' => $loadOptions,
'checkPermissions' => $this->checkPermissions,
])->indexBy('name');
namespace api\v4\Entity;
use api\v4\UnitTestCase;
+use Civi\Api4\Navigation;
use Civi\Api4\OptionGroup;
use Civi\Api4\OptionValue;
use Civi\Api4\SavedSearch;
$this->assertStringStartsWith('OptionGroup_from_email_address_OptionValue_', $result['export'][1]['name']);
}
+ public function testManagedNavigationWeights() {
+ $this->_managedEntities = [
+ [
+ 'module' => 'unit.test.fake.ext',
+ 'name' => 'Navigation_Test_Parent',
+ 'entity' => 'Navigation',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'label' => 'Test Parent',
+ 'name' => 'Test_Parent',
+ 'url' => NULL,
+ 'icon' => 'crm-i test',
+ 'permission' => 'access CiviCRM',
+ 'permission_operator' => '',
+ 'is_active' => TRUE,
+ 'weight' => 50,
+ 'parent_id' => NULL,
+ 'has_separator' => NULL,
+ 'domain_id' => 'current_domain',
+ ],
+ ],
+ ],
+ [
+ 'module' => 'unit.test.fake.ext',
+ 'name' => 'Navigation_Test_Child_1',
+ 'entity' => 'Navigation',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'label' => 'Test Child 1',
+ 'name' => 'Test_Child_1',
+ 'url' => 'civicrm/test1?reset=1',
+ 'icon' => NULL,
+ 'permission' => 'access CiviCRM',
+ 'permission_operator' => '',
+ 'parent_id.name' => 'Test_Parent',
+ 'is_active' => TRUE,
+ 'has_separator' => NULL,
+ 'domain_id' => 'current_domain',
+ ],
+ ],
+ ],
+ [
+ 'module' => 'unit.test.fake.ext',
+ 'name' => 'Navigation_Test_Child_2',
+ 'entity' => 'Navigation',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'label' => 'Test Child 2',
+ 'name' => 'Test_Child_2',
+ 'url' => 'civicrm/test2?reset=1',
+ 'icon' => NULL,
+ 'permission' => 'access CiviCRM',
+ 'permission_operator' => '',
+ 'parent_id.name' => 'Test_Parent',
+ 'is_active' => TRUE,
+ 'has_separator' => NULL,
+ 'domain_id' => 'current_domain',
+ ],
+ ],
+ ],
+ [
+ 'module' => 'unit.test.fake.ext',
+ 'name' => 'Navigation_Test_Child_3',
+ 'entity' => 'Navigation',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'label' => 'Test Child 3',
+ 'name' => 'Test_Child_3',
+ 'url' => 'civicrm/test3?reset=1',
+ 'icon' => NULL,
+ 'permission' => 'access CiviCRM',
+ 'permission_operator' => '',
+ 'parent_id.name' => 'Test_Parent',
+ 'is_active' => TRUE,
+ 'has_separator' => NULL,
+ 'domain_id' => 'current_domain',
+ ],
+ ],
+ ],
+ ];
+
+ // Refresh managed entities with module active
+ $allModules = [
+ new \CRM_Core_Module('unit.test.fake.ext', TRUE),
+ ];
+ (new \CRM_Core_ManagedEntities($allModules))->reconcile();
+
+ $nav = Navigation::get(FALSE)
+ ->addWhere('name', '=', 'Test_Parent')
+ ->addChain('export', Navigation::export()->setId('$id'))
+ ->execute()->first();
+
+ $this->assertCount(4, $nav['export']);
+ $this->assertEquals(TRUE, $nav['is_active']);
+
+ $this->assertEquals(50, $nav['export'][0]['params']['values']['weight']);
+ $this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_1', $nav['export'][1]['name']);
+ $this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_2', $nav['export'][2]['name']);
+ $this->assertEquals('Navigation_Test_Parent_Navigation_Test_Child_3', $nav['export'][3]['name']);
+ // Weight should not be included in export of children, leaving it to be auto-managed
+ $this->assertArrayNotHasKey('weight', $nav['export'][1]['params']['values']);
+
+ // Children should have been assigned correct auto-weights
+ $children = Navigation::get(FALSE)
+ ->addWhere('parent_id.name', '=', 'Test_Parent')
+ ->addOrderBy('weight')
+ ->execute();
+ foreach ([1, 2, 3] as $index => $weight) {
+ $this->assertEquals($weight, $children[$index]['weight']);
+ $this->assertEquals(TRUE, $children[$index]['is_active']);
+ }
+
+ // Refresh managed entities with module disabled
+ $allModules = [
+ new \CRM_Core_Module('unit.test.fake.ext', FALSE),
+ ];
+ (new \CRM_Core_ManagedEntities($allModules))->reconcile();
+
+ // Children's weight should have been unaffected, but they should be disabled
+ $children = Navigation::get(FALSE)
+ ->addWhere('parent_id.name', '=', 'Test_Parent')
+ ->addOrderBy('weight')
+ ->execute();
+ foreach ([1, 2, 3] as $index => $weight) {
+ $this->assertEquals($weight, $children[$index]['weight']);
+ $this->assertEquals(FALSE, $children[$index]['is_active']);
+ }
+
+ $nav = Navigation::get(FALSE)
+ ->addWhere('name', '=', 'Test_Parent')
+ ->execute()->first();
+ $this->assertEquals(FALSE, $nav['is_active']);
+
+ // Refresh managed entities with module active
+ $allModules = [
+ new \CRM_Core_Module('unit.test.fake.ext', TRUE),
+ ];
+ (new \CRM_Core_ManagedEntities($allModules))->reconcile();
+
+ // Children's weight should have been unaffected, but they should be enabled
+ $children = Navigation::get(FALSE)
+ ->addWhere('parent_id.name', '=', 'Test_Parent')
+ ->addOrderBy('weight')
+ ->execute();
+ foreach ([1, 2, 3] as $index => $weight) {
+ $this->assertEquals($weight, $children[$index]['weight']);
+ $this->assertEquals(TRUE, $children[$index]['is_active']);
+ }
+ // Parent should also be re-enabled
+ $nav = Navigation::get(FALSE)
+ ->addWhere('name', '=', 'Test_Parent')
+ ->execute()->first();
+ $this->assertEquals(TRUE, $nav['is_active']);
+ }
+
/**
* @dataProvider sampleEntityTypes
* @param string $entityName
'CustomField' => TRUE,
'CustomGroup' => TRUE,
'MembershipType' => TRUE,
+ 'Navigation' => TRUE,
'OptionGroup' => TRUE,
'OptionValue' => TRUE,
'SavedSearch' => TRUE,