diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php index 102fa70..9c9d123 100644 --- a/core/includes/entity.api.php +++ b/core/includes/entity.api.php @@ -34,6 +34,10 @@ * different operations, the name of the operation is passed also to the * constructor of the form controller class. This way, one class can be used * for multiple entity forms. + * - list controller class: The name of the class that is used to provide + * listings of the entity. The class must implement + * Drupal\Core\Entity\EntityListControllerInterface. Defaults to + * Drupal\Core\Entity\EntityListController. * - base table: (used by Drupal\Core\Entity\DatabaseStorageController) The * name of the entity type's base table. * - static cache: (used by Drupal\Core\Entity\DatabaseStorageController) diff --git a/core/includes/entity.inc b/core/includes/entity.inc index b651e32..2b504aa 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -47,6 +47,7 @@ function entity_get_info($entity_type = NULL) { 'fieldable' => FALSE, 'entity class' => 'Drupal\Core\Entity\Entity', 'controller class' => 'Drupal\Core\Entity\DatabaseStorageController', + 'list controller class' => 'Drupal\Core\Entity\EntityListController', 'form controller class' => array( 'default' => 'Drupal\Core\Entity\EntityFormController', ), @@ -530,3 +531,21 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st field_attach_submit($entity_type, $entity, $form, $form_state); } } + +/** + * Returns an entity list controller for a given entity type. + * + * @param string $entity_type + * The type of the entity. + * + * @return Drupal\Core\Entity\EntityListControllerInterface + * An entity list controller. + * + * @see hook_entity_info() + */ +function entity_list_controller($entity_type) { + $storage = entity_get_controller($entity_type); + $entity_info = entity_get_info($entity_type); + $class = $entity_info['list controller class']; + return new $class($entity_type, $storage); +} diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php new file mode 100644 index 0000000..3bd4b82 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityListController.php @@ -0,0 +1,27 @@ +entityType = $entity_type; + $this->storage = $storage; + $this->entityInfo = entity_get_info($this->entityType); + } + + /** + * Implements Drupal\Core\Entity\EntityListControllerInterface::getStorageController(). + */ + public function getStorageController() { + return $this->storage; + } + + /** + * Implements Drupal\Core\Entity\EntityListControllerInterface::load(). + */ + public function load() { + return $this->storage->load(); + } + + /** + * Implements Drupal\Core\Entity\EntityListControllerInterface::getOperations(). + */ + public function getOperations(EntityInterface $entity) { + $uri = $entity->uri(); + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $uri['path'] . '/edit', + 'options' => $uri['options'], + 'weight' => 10, + ); + $operations['delete'] = array( + 'title' => t('Delete'), + 'href' => $uri['path'] . '/delete', + 'options' => $uri['options'], + 'weight' => 100, + ); + return $operations; + } + + /** + * Builds the header row for the entity listing. + * + * @return array + * A render array structure of header strings. + * + * @see Drupal\Core\Entity\EntityListController::render() + */ + public function buildHeader() { + $row['label'] = t('Label'); + $row['id'] = t('Machine name'); + $row['operations'] = t('Operations'); + return $row; + } + + /** + * Builds a row for an entity in the entity listing. + * + * @param Drupal\Core\Entity\EntityInterface $entity + * The entity for this row of the list. + * + * @return array + * A render array structure of fields for this entity. + * + * @see Drupal\Core\Entity\EntityListController::render() + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + $operations = $this->buildOperations($entity); + $row['operations']['data'] = $operations; + return $row; + } + + /** + * Builds a renderable list of operation links for the entity. + * + * @param Drupal\Core\Entity\EntityInterface $entity + * The entity on which the linked operations will be performed. + * + * @return array + * A renderable array of operation links. + * + * @see Drupal\Core\Entity\EntityListController::render() + */ + public function buildOperations(EntityInterface $entity) { + // Retrieve and sort operations. + $operations = $this->getOperations($entity); + uasort($operations, 'drupal_sort_weight'); + $build = array( + '#theme' => 'links', + '#links' => $operations, + ); + return $build; + } + + /** + * Implements Drupal\Core\Entity\EntityListControllerInterface::render(). + * + * Builds the entity list as renderable array for theme_table(). + * + * @todo Add a link to add a new item to the #empty text. + */ + public function render() { + $build = array( + '#theme' => 'table', + '#header' => $this->buildHeader(), + '#rows' => array(), + '#empty' => t('There is no @label yet.', array('@label' => $this->entityInfo['label'])), + ); + foreach ($this->load() as $entity) { + $build['#rows'][$entity->id()] = $this->buildRow($entity); + } + return $build; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityListControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityListControllerInterface.php new file mode 100644 index 0000000..85790c3 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityListControllerInterface.php @@ -0,0 +1,58 @@ + 'Configuration entity list', + 'description' => 'Tests the listing of configuration entities.', + 'group' => 'Configuration', + ); + } + + /** + * Tests entity list controller methods. + */ + function testList() { + $controller = entity_list_controller('config_test'); + + // Test getStorageController() method. + $this->assertTrue($controller->getStorageController() instanceof EntityStorageControllerInterface, 'EntityStorageController instance in storage.'); + + // Get a list of ConfigTest entities and confirm that it contains the + // ConfigTest entity provided by the config_test module. + // @see config_test.dynamic.default.yml + $list = $controller->load(); + $this->assertEqual(count($list), 1, '1 ConfigTest entity found.'); + $entity = $list['default']; + $this->assertTrue(!empty($entity), '"Default" ConfigTest entity ID found.'); + $this->assertTrue($entity instanceof ConfigTest, '"Default" ConfigTest entity is an instance of ConfigTest.'); + + // Test getOperations() method. + $uri = $entity->uri(); + $expected_operations = array( + 'edit' => array ( + 'title' => 'Edit', + 'href' => 'admin/structure/config_test/manage/default/edit', + 'options' => $uri['options'], + 'weight' => 10, + ), + 'delete' => array ( + 'title' => 'Delete', + 'href' => 'admin/structure/config_test/manage/default/delete', + 'options' => $uri['options'], + 'weight' => 100, + ), + ); + $actual_operations = $controller->getOperations($entity); + $this->assertIdentical($expected_operations, $actual_operations, 'Return value from getOperations matches expected.'); + + // Test buildHeader() method. + $expected_items = array( + 'label' => 'Label', + 'id' => 'Machine name', + 'operations' => 'Operations', + ); + $actual_items = $controller->buildHeader(); + $this->assertIdentical($expected_items, $actual_items, 'Return value from buildHeader matches expected.'); + + // Test buildRow() method. + $build_operations = $controller->buildOperations($entity); + $expected_items = array( + 'label' => 'Default', + 'id' => 'default', + 'operations' => array( + 'data' => $build_operations, + ), + ); + $actual_items = $controller->buildRow($entity); + $this->assertIdentical($expected_items, $actual_items, 'Return value from buildRow matches expected.'); + + // Test sorting. + $entity = entity_create('config_test', array( + 'id' => 'albatros', + 'label' => 'Albatros', + 'weight' => 1, + )); + $entity->save(); + $entity = entity_create('config_test', array( + 'id' => 'antelope', + 'label' => 'Antelope', + 'weight' => 1, + )); + $entity->save(); + $list = $controller->load(); + $entity = end($list); + $this->assertEqual($entity->weight, 1); + } + + /** + * Tests the listing UI. + */ + function testListUI() { + // Log in as an administrative user to access the full menu trail. + $this->drupalLogin($this->drupalCreateUser(array('access administration pages'))); + + // Get the list callback page. + $this->drupalGet('admin/structure/config_test'); + + // Test for the page title. + $this->assertTitle('Test configuration | Drupal'); + + // Test for the table. + $element = $this->xpath('//div[@id="content"]//table'); + $this->assertTrue($element, 'Configuration entity list table found.'); + + // Test the table header. + $elements = $this->xpath('//div[@id="content"]//table/thead/tr/th'); + $this->assertEqual(count($elements), 3, 'Correct number of table header cells found.'); + + // Test the contents of each th cell. + $expected_items = array('Label', 'Machine name', 'Operations'); + foreach ($elements as $key => $element) { + $this->assertIdentical((string) $element[0], $expected_items[$key]); + } + + // Check the number of table row cells. + $elements = $this->xpath('//div[@id="content"]//table/tbody/tr[@class="odd"]/td'); + $this->assertEqual(count($elements), 3, 'Correct number of table row cells found.'); + + // Check the contents of each row cell. The first cell contains the label, + // the second contains the machine name, and the third contains the + // operations list. + $this->assertIdentical((string) $elements[0], 'Default'); + $this->assertIdentical((string) $elements[1], 'default'); + $this->assertTrue($elements[2]->children()->xpath('//ul'), 'Operations list found.'); + + // Add a new entity using the operations link. + $this->assertLink('Add test configuration'); + $this->clickLink('Add test configuration'); + $this->assertResponse(200); + $edit = array('label' => 'Antelope', 'id' => 'antelope'); + $this->drupalPost(NULL, $edit, t('Save')); + + // Confirm that the user is returned to the listing, and verify that the + // text of the label and machine name appears in the list (versus elsewhere + // on the page). + $this->assertFieldByXpath('//td', 'Antelope', "Label found for added 'Antelope' entity."); + $this->assertFieldByXpath('//td', 'antelope', "Machine name found for added 'Antelope' entity."); + + // Edit the entity using the operations link. + $this->assertLink('Edit'); + $this->clickLink('Edit'); + $this->assertResponse(200); + $this->assertTitle('Edit test configuration | Drupal'); + $edit = array( + 'label' => 'Albatross', + 'id' => 'albatross', + 'weight' => 1, + ); + $this->drupalPost(NULL, $edit, t('Save')); + + // Confirm that the user is returned to the listing, and verify that the + // text of the label and machine name appears in the list (versus elsewhere + // on the page). + $this->assertFieldByXpath('//td', 'Albatross', "Label found for updated 'Albatross' entity."); + $this->assertFieldByXpath('//td', 'albatross', "Machine name found for updated 'Albatross' entity."); + + // Delete the added entity using the operations link. + $this->assertLink('Delete', 1); + $this->clickLink('Delete', 1); + $this->assertResponse(200); + $this->assertTitle('Are you sure you want to delete Albatross | Drupal'); + $this->drupalPost(NULL, array(), t('Delete')); + + // Verify that the text of the label and machine name does not appear in + // the list (though it may appear elsewhere on the page). + $this->assertNoFieldByXpath('//td', 'Albatross', "No label found for deleted 'Albatross' entity."); + $this->assertNoFieldByXpath('//td', 'albatross', "No machine name found for deleted 'Albatross' entity."); + + // Delete the original entity using the operations link. + $this->clickLink('Delete'); + $this->assertResponse(200); + $this->assertTitle('Are you sure you want to delete Default | Drupal'); + $this->drupalPost(NULL, array(), t('Delete')); + + // Verify that the text of the label and machine name does not appear in + // the list (though it may appear elsewhere on the page). + $this->assertNoFieldByXpath('//td', 'Default', "No label found for deleted 'Default' entity."); + $this->assertNoFieldByXpath('//td', 'default', "No machine name found for deleted 'Default' entity."); + + // Confirm that the empty text is displayed. + $this->assertText('There is no Test configuration yet.'); + } + +} diff --git a/core/modules/config/tests/config_test/config/config_test.dynamic.default.yml b/core/modules/config/tests/config_test/config/config_test.dynamic.default.yml index 3e50e3b..c22273b 100644 --- a/core/modules/config/tests/config_test/config/config_test.dynamic.default.yml +++ b/core/modules/config/tests/config_test/config/config_test.dynamic.default.yml @@ -1,2 +1,3 @@ id: default label: Default +weight: 0 diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module index 61d92df..670124b 100644 --- a/core/modules/config/tests/config_test/config_test.module +++ b/core/modules/config/tests/config_test/config_test.module @@ -82,6 +82,7 @@ function config_test_entity_info() { 'label' => 'Test configuration', 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController', 'entity class' => 'Drupal\config_test\ConfigTest', + 'list controller class' => 'Drupal\Core\Config\Entity\ConfigEntityListController', 'uri callback' => 'config_test_uri', 'config prefix' => 'config_test.dynamic', 'entity keys' => array( @@ -176,36 +177,8 @@ function config_test_delete($id) { * Page callback; Lists available ConfigTest objects. */ function config_test_list_page() { - $entities = entity_load_multiple('config_test'); - uasort($entities, 'Drupal\Core\Config\Entity\ConfigEntityBase::sort'); - - $rows = array(); - foreach ($entities as $config_test) { - $uri = $config_test->uri(); - $row = array(); - $row['name']['data'] = array( - '#type' => 'link', - '#title' => $config_test->label(), - '#href' => $uri['path'], - '#options' => $uri['options'], - ); - $row['delete']['data'] = array( - '#type' => 'link', - '#title' => t('Delete'), - '#href' => $uri['path'] . '/delete', - '#options' => $uri['options'], - ); - $rows[] = $row; - } - $build = array( - '#theme' => 'table', - '#header' => array('Name', 'Operations'), - '#rows' => $rows, - '#empty' => format_string('No test configuration defined. Add some', array( - '@add-url' => url('admin/structure/config_test/add'), - )), - ); - return $build; + $controller = entity_list_controller('config_test'); + return $controller->render(); } /** @@ -245,6 +218,11 @@ function config_test_form($form, &$form_state, ConfigTest $config_test = NULL) { 'source' => array('label'), ), ); + $form['weight'] = array( + '#type' => 'weight', + '#title' => 'Weight', + '#default_value' => $config_test->weight, + ); $form['style'] = array( '#type' => 'select', '#title' => 'Image style', diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php index e1df5c4..a4c7cbf 100644 --- a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php @@ -42,4 +42,11 @@ class ConfigTest extends ConfigEntityBase { */ public $style; + /** + * The weight of the configuration entity in relation to others. + * + * @var integer + */ + public $weight = 0; + }