diff --git a/core/modules/views_ui/css/views_ui.admin.theme.css b/core/modules/views_ui/css/views_ui.admin.theme.css index 4ab55c46575400f2c20db3d85dd0740654c743d0..460e75eaa2adcf9b6a6256189b64a4995fcb30c6 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -170,9 +170,9 @@ details.box-padding { margin-bottom: 6px; margin-top: 6px; } -.views-ui-view-title { +.views-ui-view-name h3 { font-weight: bold; - margin-top: 0; + margin: 0.25em 0; } .view-changed { margin-bottom: 21px; @@ -183,22 +183,33 @@ details.box-padding { margin-bottom: 0; margin-top: 18px; } +.views-ui-view-displays ul { + margin-left: 0; /* LTR */ + padding-left: 0; /* LTR */ + list-style: none; +} +[dir="rtl"] .views-ui-view-displays ul { + margin-right: 0; + padding-right: 0; + margin-left: inherit; + padding-left: inherit; +} /* These header classes are ambiguous and should be scoped to th elements */ .views-ui-name { - width: 18%; + width: 20%; } .views-ui-description { - width: 26%; + width: 30%; } -.views-ui-tag { - width: 8%; +.views-ui-machine-name { + width: 15%; } -.views-ui-path { - width: auto; +.views-ui-displays { + width: 25%; } .views-ui-operations { - width: 24%; + width: 10%; } /** diff --git a/core/modules/views_ui/src/Tests/DefaultViewsTest.php b/core/modules/views_ui/src/Tests/DefaultViewsTest.php index 3b3f2a734327254399ba00288446137976f98f41..08fcae8ea4eef534935c48eb8eb45b8ad5f8d2be 100644 --- a/core/modules/views_ui/src/Tests/DefaultViewsTest.php +++ b/core/modules/views_ui/src/Tests/DefaultViewsTest.php @@ -161,10 +161,11 @@ function testDefaultViews() { */ function testSplitListing() { // Build a re-usable xpath query. - $xpath = '//div[@id="views-entity-list"]/div[@class = :status]/table//tr[@title = :title]'; + $xpath = '//div[@id="views-entity-list"]/div[@class = :status]/table//td/text()[contains(., :title)]'; + $arguments = array( ':status' => 'views-list-section enabled', - ':title' => t('Machine name: test_view_status'), + ':title' => 'test_view_status', ); $this->drupalGet('admin/structure/views'); diff --git a/core/modules/views_ui/src/Tests/XssTest.php b/core/modules/views_ui/src/Tests/XssTest.php index 8eac3a687368f282f31d9e6b86786c9378c9dbd5..1d61f1942725e7ed964441f26429bf33232bbc94 100644 --- a/core/modules/views_ui/src/Tests/XssTest.php +++ b/core/modules/views_ui/src/Tests/XssTest.php @@ -17,9 +17,6 @@ class XssTest extends UITestBase { public static $modules = array('node', 'user', 'views_ui', 'views_ui_test'); public function testViewsUi() { - $this->drupalGet('admin/structure/views'); - $this->assertEscaped(', test', 'The view tag is properly escaped.'); - $this->drupalGet('admin/structure/views/view/sa_contrib_2013_035'); $this->assertEscaped('test', 'Field admin label is properly escaped.'); diff --git a/core/modules/views_ui/src/ViewListBuilder.php b/core/modules/views_ui/src/ViewListBuilder.php index 112e1c457921daa349d9a27d0b17a03e70038936..b38a15f75bdf9e0331fe6374c26dc1b001aa3f81 100644 --- a/core/modules/views_ui/src/ViewListBuilder.php +++ b/core/modules/views_ui/src/ViewListBuilder.php @@ -89,34 +89,30 @@ public function buildRow(EntityInterface $view) { 'data' => array( 'view_name' => array( 'data' => array( - '#theme' => 'views_ui_view_info', - '#view' => $view, - '#displays' => $this->getDisplaysList($view) + '#plain_text' => $view->label(), ), ), - 'description' => array( + 'machine_name' => array( 'data' => array( - '#plain_text' => $view->get('description'), + '#plain_text' => $view->id(), ), - 'data-drupal-selector' => 'views-table-filter-text-source', ), - 'tag' => array( + 'description' => array( 'data' => array( - '#plain_text' => $view->get('tag'), + '#plain_text' => $view->get('description'), ), - 'data-drupal-selector' => 'views-table-filter-text-source', ), - 'path' => array( + 'displays' => array( 'data' => array( - '#theme' => 'item_list', - '#items' => $this->getDisplayPaths($view), - '#context' => ['list_style' => 'comma-list'], + '#theme' => 'views_ui_view_displays_list', + '#displays' => $this->getDisplaysList($view), ), ), 'operations' => $row['operations'], ), - 'title' => $this->t('Machine name: @name', array('@name' => $view->id())), - 'class' => array($view->status() ? 'views-ui-list-enabled' : 'views-ui-list-disabled'), + '#attributes' => array( + 'class' => array($view->status() ? 'views-ui-list-enabled' : 'views-ui-list-disabled'), + ), ); } @@ -127,23 +123,33 @@ public function buildHeader() { return array( 'view_name' => array( 'data' => $this->t('View name'), - 'class' => array('views-ui-name'), + '#attributes' => array( + 'class' => array('views-ui-name'), + ), + ), + 'machine_name' => array( + 'data' => $this->t('Machine name'), + '#attributes' => array( + 'class' => array('views-ui-machine-name'), + ), ), 'description' => array( 'data' => $this->t('Description'), - 'class' => array('views-ui-description'), - ), - 'tag' => array( - 'data' => $this->t('Tag'), - 'class' => array('views-ui-tag'), + '#attributes' => array( + 'class' => array('views-ui-description'), + ), ), - 'path' => array( - 'data' => $this->t('Path'), - 'class' => array('views-ui-path'), + 'displays' => array( + 'data' => $this->t('Displays'), + '#attributes' => array( + 'class' => array('views-ui-displays'), + ), ), 'operations' => array( 'data' => $this->t('Operations'), - 'class' => array('views-ui-operations'), + '#attributes' => array( + 'class' => array('views-ui-operations'), + ), ), ); } @@ -196,13 +202,13 @@ public function render() { '#type' => 'search', '#title' => $this->t('Filter'), '#title_display' => 'invisible', - '#size' => 40, - '#placeholder' => $this->t('Filter by view name or description'), + '#size' => 60, + '#placeholder' => $this->t('Filter by view name, machine name, description, or display path'), '#attributes' => array( 'class' => array('views-filter-text'), 'data-table' => '.views-listing-table', 'autocomplete' => 'off', - 'title' => $this->t('Enter a part of the view name or description to filter by.'), + 'title' => $this->t('Enter a part of the view name, machine name, description, or display path to filter by.'), ), ); @@ -212,12 +218,9 @@ public function render() { $list[$status]['#type'] = 'container'; $list[$status]['#attributes'] = array('class' => array('views-list-section', $status)); $list[$status]['table'] = array( - '#type' => 'table', - '#attributes' => array( - 'class' => array('views-listing-table'), - ), - '#header' => $this->buildHeader(), - '#rows' => array(), + '#theme' => 'views_ui_views_listing_table', + '#headers' => $this->buildHeader(), + '#attributes' => array('class' => array('views-listing-table', $status)), ); foreach ($entities[$status] as $entity) { $list[$status]['table']['#rows'][$entity->id()] = $this->buildRow($entity); @@ -242,46 +245,33 @@ public function render() { */ protected function getDisplaysList(EntityInterface $view) { $displays = array(); - foreach ($view->get('display') as $display) { - $definition = $this->displayManager->getDefinition($display['display_plugin']); - if (!empty($definition['admin'])) { - // Cast the admin label to a string since it is an object. - // @see \Drupal\Core\StringTranslation\TranslatableMarkup - $displays[] = (string) $definition['admin']; - } - } - - sort($displays); - return $displays; - } - /** - * Gets a list of paths assigned to the view. - * - * @param \Drupal\Core\Entity\EntityInterface $view - * The view entity. - * - * @return array - * An array of paths for this view. - */ - protected function getDisplayPaths(EntityInterface $view) { - $all_paths = array(); $executable = $view->getExecutable(); $executable->initDisplay(); foreach ($executable->displayHandlers as $display) { - if ($display->hasPath()) { - $path = $display->getPath(); - if ($view->status() && strpos($path, '%') === FALSE) { - // @todo Views should expect and store a leading /. See: - // https://www.drupal.org/node/2423913 - $all_paths[] = \Drupal::l('/' . $path, Url::fromUserInput('/' . $path)); - } - else { - $all_paths[] = '/' . $path; + $rendered_path = FALSE; + $definition = $display->getPluginDefinition(); + if (!empty($definition['admin'])) { + if ($display->hasPath()) { + $path = $display->getPath(); + if ($view->status() && strpos($path, '%') === FALSE) { + // @todo Views should expect and store a leading /. See: + // https://www.drupal.org/node/2423913 + $rendered_path = \Drupal::l('/' . $path, Url::fromUserInput('/' . $path)); + } + else { + $rendered_path = '/' . $path; + } } + $displays[] = array( + 'display' => $definition['admin'], + 'path' => $rendered_path, + ); } } - return array_unique($all_paths); + + sort($displays); + return $displays; } } diff --git a/core/modules/views_ui/templates/views-ui-view-displays-list.html.twig b/core/modules/views_ui/templates/views-ui-view-displays-list.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..1c7d6210078c6868d4c0341a3072469c3d6dbc11 --- /dev/null +++ b/core/modules/views_ui/templates/views-ui-view-displays-list.html.twig @@ -0,0 +1,24 @@ +{# +/** + * @file + * Default theme implementation for views displays on the views listing page. + * + * Available variables: + * - displays: Contains multiple display instances. Each display contains: + * - display: Display name. + * - path: Path to display, if any. + * + * @ingroup themeable + */ +#} + diff --git a/core/modules/views_ui/templates/views-ui-views-listing-table.html.twig b/core/modules/views_ui/templates/views-ui-views-listing-table.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..207462e88bccc1b77b84d562aec1c872236d272e --- /dev/null +++ b/core/modules/views_ui/templates/views-ui-views-listing-table.html.twig @@ -0,0 +1,49 @@ +{# +/** + * @file + * Default theme implementation for views listing table. + * + * Available variables: + * - headers: Contains table headers. + * - rows: Contains multiple rows. Each row contains: + * - view_name: The human-readable name of the view. + * - machine_name: Machine name of the view. + * - description: The description of the view. + * - displays: List of displays attached to the view. + * - operations: List of available operations. + * + * @see template_preprocess_views_ui_views_listing_table() + * + * @ingroup themeable + */ +#} + + + + {% for header in headers %} + {{ header.data }} + {% endfor %} + + + + {% for row in rows %} + + +

{{ row.data.view_name.data }}

+ + + {{ row.data.machine_name.data }} + + + {{ row.data.description.data }} + + + {{ row.data.displays.data }} + + + {{ row.data.operations.data }} + + + {% endfor %} + + diff --git a/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php b/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php index 215728cd5f43af7e0047b780beea7795ab79314a..9b646964eb04dec4228eb13fb782b5c99628345b 100644 --- a/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php +++ b/core/modules/views_ui/tests/src/Unit/ViewListBuilderTest.php @@ -152,21 +152,34 @@ public function testBuildRowEntityList() { $view_list_builder = new TestViewListBuilder($entity_type, $storage, $display_manager); $view_list_builder->setStringTranslation($this->getStringTranslationStub()); + // Create new view with test values. $view = new View($values, 'view'); + // Get the row object created by ViewListBuilder for this test view. $row = $view_list_builder->buildRow($view); + // Expected output array for view's displays. $expected_displays = array( - 'Embed admin label', - 'Page admin label', - 'Page admin label', - 'Page admin label', + '0' => array( + 'display' => 'Embed admin label', + 'path' => FALSE, + ), + '1' => array( + 'display' => 'Page admin label', + 'path' => '/malformed_path', + ), + '2' => array( + 'display' => 'Page admin label', + 'path' => '/', + ), + '3' => array( + 'display' => 'Page admin label', + 'path' => '/test_page', + ), ); - $this->assertEquals($expected_displays, $row['data']['view_name']['data']['#displays']); - $display_paths = $row['data']['path']['data']['#items']; - // These values will be escaped by Twig when rendered. - $this->assertEquals('/test_page, /malformed_path, /', implode(', ', $display_paths)); + // Compare the expected and generated output. + $this->assertEquals($expected_displays, $row['data']['displays']['data']['#displays']); } } diff --git a/core/modules/views_ui/views_ui.module b/core/modules/views_ui/views_ui.module index 497636809baa71b341b1a9977e334b0e174e9ac4..165e8669efb4cc9c53a91e90038a4b536287b83e 100644 --- a/core/modules/views_ui/views_ui.module +++ b/core/modules/views_ui/views_ui.module @@ -80,12 +80,25 @@ function views_ui_theme() { 'file' => 'views_ui.theme.inc', ), - // list views + // Legacy theme hook for displaying views info. 'views_ui_view_info' => array( 'variables' => array('view' => NULL, 'displays' => NULL), 'file' => 'views_ui.theme.inc', ), + // List views. + 'views_ui_views_listing_table' => array( + 'variables' => array( + 'headers' => NULL, + 'rows' => NULL, + 'attributes' => array(), + ), + 'file' => 'views_ui.theme.inc', + ), + 'views_ui_view_displays_list' => array( + 'variables' => array('displays' => array()), + ), + // Group of filters. 'views_ui_build_group_filter_form' => array( 'render element' => 'form', diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc index 3b5867894d39721e4e8386d868aee6e7ad9bc01c..926f82ab309d439ff5e953d122c74a9a028dbf41 100644 --- a/core/modules/views_ui/views_ui.theme.inc +++ b/core/modules/views_ui/views_ui.theme.inc @@ -11,6 +11,7 @@ use Drupal\Core\Render\Element\Checkboxes; use Drupal\Core\Render\Element\Radios; use Drupal\Core\Url; +use Drupal\Core\Template\Attribute; /** * Prepares variables for Views UI display tab setting templates. @@ -42,6 +43,31 @@ function template_preprocess_views_ui_display_tab_setting(&$variables) { } } +/** + * Prepares variables for Views UI view listing templates. + * + * Default template: views-ui-view-listing-table.html.twig. + * + * @param array $variables + * An associative array containing: + * - headers: An associative array containing the headers for the view + * listing table. + * - rows: An associative array containing the rows data for the view + * listing table. + */ +function template_preprocess_views_ui_views_listing_table(&$variables) { + // Convert the attributes to valid attribute objects. + foreach ($variables['headers'] as $key => $header) { + $variables['headers'][$key]['attributes'] = new Attribute($header['#attributes']); + } + + if (!empty($variables['rows'])) { + foreach ($variables['rows'] as $key => $row) { + $variables['rows'][$key]['attributes'] = new Attribute($row['#attributes']); + } + } +} + /** * Prepares variables for Views UI display tab bucket templates. * diff --git a/core/themes/stable/css/views_ui/views_ui.admin.theme.css b/core/themes/stable/css/views_ui/views_ui.admin.theme.css index 3bf3290c88967a9a27a8c2a65e49e86cad36421d..3a2afb56459d98a8302f5d8eafa790bc51dd34c4 100644 --- a/core/themes/stable/css/views_ui/views_ui.admin.theme.css +++ b/core/themes/stable/css/views_ui/views_ui.admin.theme.css @@ -170,9 +170,9 @@ details.box-padding { margin-bottom: 6px; margin-top: 6px; } -.views-ui-view-title { +.views-ui-view-name h3 { font-weight: bold; - margin-top: 0; + margin: 0.25em 0; } .view-changed { margin-bottom: 21px; @@ -183,22 +183,33 @@ details.box-padding { margin-bottom: 0; margin-top: 18px; } +.views-ui-view-displays ul { + margin-left: 0; /* LTR */ + padding-left: 0; /* LTR */ + list-style: none; +} +[dir="rtl"] .views-ui-view-displays ul { + margin-right: 0; + padding-right: 0; + margin-left: inherit; + padding-left: inherit; +} /* These header classes are ambiguous and should be scoped to th elements */ .views-ui-name { - width: 18%; + width: 20%; } .views-ui-description { - width: 26%; + width: 30%; } -.views-ui-tag { - width: 8%; +.views-ui-machine-name { + width: 15%; } -.views-ui-path { - width: auto; +.views-ui-displays { + width: 25%; } .views-ui-operations { - width: 24%; + width: 10%; } /** diff --git a/core/themes/stable/templates/admin/views-ui-view-displays-list.html.twig b/core/themes/stable/templates/admin/views-ui-view-displays-list.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..e6e5b021270a47ca3afea904722e1e197b617805 --- /dev/null +++ b/core/themes/stable/templates/admin/views-ui-view-displays-list.html.twig @@ -0,0 +1,22 @@ +{# +/** + * @file + * Theme override for views displays on the views listing page. + * + * Available variables: + * - displays: Contains multiple display instances. Each display contains: + * - display: Display name. + * - path: Path to display, if any. + */ +#} +
    + {% for display in displays %} +
  • + {% if display.path %} + {{ display.display }} ({{ display.path }}) + {% else %} + {{ display.display }} + {% endif %} +
  • + {% endfor %} +
diff --git a/core/themes/stable/templates/admin/views-ui-views-listing-table.html.twig b/core/themes/stable/templates/admin/views-ui-views-listing-table.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..bc12a0a6efba881a374f55e368c63e0e3aff9d94 --- /dev/null +++ b/core/themes/stable/templates/admin/views-ui-views-listing-table.html.twig @@ -0,0 +1,47 @@ +{# +/** + * @file + * Theme override for views listing table. + * + * Available variables: + * - headers: Contains table headers. + * - rows: Contains multiple rows. Each row contains: + * - view_name: The human-readable name of the view. + * - machine_name: Machine name of the view. + * - description: The description of the view. + * - displays: List of displays attached to the view. + * - operations: List of available operations. + * + * @see template_preprocess_views_ui_views_listing_table() + */ +#} + + + + {% for header in headers %} + {{ header.data }} + {% endfor %} + + + + {% for row in rows %} + + +

{{ row.data.view_name.data }}

+ + + {{ row.data.machine_name.data }} + + + {{ row.data.description.data }} + + + {{ row.data.displays.data }} + + + {{ row.data.operations.data }} + + + {% endfor %} + +