diff --git a/css/overview.css b/css/overview.css new file mode 100644 index 0000000..f53c610 --- /dev/null +++ b/css/overview.css @@ -0,0 +1,10 @@ +.overview-details { + border: 0; + margin: 0; +} + +.overview-details summary { + font-weight: normal; + text-transform: none; + padding: 0; +} diff --git a/js/overview.js b/js/overview.js new file mode 100644 index 0000000..a11712f --- /dev/null +++ b/js/overview.js @@ -0,0 +1,67 @@ +/** + * @file + * Paragraphs Collection overview behaviors. + */ + +(function ($, Drupal) { + + "use strict"; + + /** + * Filters the overview table by input search filters. + * + * Target table: .table-filter[data-table] + * Text search input: input.table-filter-text + * Group search select: select.table-filter-group-select + * Source text: .table-filter-text-source + * Source group: .table-filter-group-source + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.tableFilterByText = { + attach: function (context, settings) { + var $filters = $('.table-filter').once('table-filter'); + var $table = $($filters.attr('data-table')); + // var $selects = $ + var $text_input = $('input.table-filter-text').once('table-filter-text'); + var $group_select = $('select.table-filter-group-select').once('table-filter-group-select'); + var $rows; + + function filterItemList() { + var group_value; + if (typeof $group_select.val() !== 'undefined') { + group_value = $group_select.val().toLowerCase(); + } + var text_value = $text_input.val().toLowerCase(); + + function showItemRow(index, row) { + var $row = $(row); + var $group_sources = $row.find('.table-filter-group-source'); + var $text_sources = $row.find('.table-filter-text-source'); + var group_array = $group_sources.map(function() { + return $(this).text().toLowerCase(); + }).get(); + + if (group_value && group_array.indexOf(group_value) == -1) { + $row.hide(); + return; + } + if (text_value && $text_sources.text().toLowerCase().indexOf(text_value) == -1) { + $row.hide(); + return; + } + $row.show(); + } + + $rows.each(showItemRow); + } + + if ($table.length) { + $rows = $table.find('tbody tr'); + $text_input.on('keyup', filterItemList); + $group_select.on('change', filterItemList); + } + } + }; + +}(jQuery, Drupal)); diff --git a/paragraphs_collection.libraries.yml b/paragraphs_collection.libraries.yml index bddd5ea..298bdec 100644 --- a/paragraphs_collection.libraries.yml +++ b/paragraphs_collection.libraries.yml @@ -1,3 +1,14 @@ +overview: + css: + component: + css/overview.css: {} + js: + js/overview.js: {} + dependencies: + - core/drupal + - core/jquery + - core/jquery.once + grid_layout: css: theme: diff --git a/paragraphs_collection.links.menu.yml b/paragraphs_collection.links.menu.yml new file mode 100644 index 0000000..70aeea3 --- /dev/null +++ b/paragraphs_collection.links.menu.yml @@ -0,0 +1,6 @@ +paragraphs_collection.layouts: + title: 'Paragraphs Collection' + parent: system.admin_reports + description: 'Overviews of items discoverable by behavior plugins.' + route_name: paragraphs_collection.layouts + menu_name: admin diff --git a/paragraphs_collection.links.task.yml b/paragraphs_collection.links.task.yml new file mode 100644 index 0000000..a7742b7 --- /dev/null +++ b/paragraphs_collection.links.task.yml @@ -0,0 +1,10 @@ +paragraphs_collection.layouts: + title: Layouts + route_name: paragraphs_collection.layouts + base_route: paragraphs_collection.layouts + +paragraphs_collection.styles: + title: Styles + route_name: paragraphs_collection.styles + base_route: paragraphs_collection.layouts + diff --git a/paragraphs_collection.routing.yml b/paragraphs_collection.routing.yml new file mode 100644 index 0000000..6a87acf --- /dev/null +++ b/paragraphs_collection.routing.yml @@ -0,0 +1,15 @@ +paragraphs_collection.layouts: + path: '/admin/reports/paragraphs_collection/layouts' + defaults: + _controller: '\Drupal\paragraphs_collection\Controller\OverviewController::layouts' + _title: 'Available grid layouts' + requirements: + _permission: 'administer paragraphs types' + +paragraphs_collection.styles: + path: '/admin/reports/paragraphs_collection/styles' + defaults: + _controller: '\Drupal\paragraphs_collection\Controller\OverviewController::styles' + _title: 'Available styles' + requirements: + _permission: 'administer paragraphs types' diff --git a/src/Controller/OverviewController.php b/src/Controller/OverviewController.php new file mode 100644 index 0000000..7d7be15 --- /dev/null +++ b/src/Controller/OverviewController.php @@ -0,0 +1,427 @@ +gridLayoutDiscovery = $grid_layout_discovery; + $this->styleDiscovery = $style_discovery; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('paragraphs_collection.grid_layout_discovery'), + $container->get('paragraphs_collection.style_discovery') + ); + } + + /** + * Lists Paragraphs Types with the grid layouts they allow. + * + * @return array + * A nested array. The first level is keyed by Paragraphs Type IDs. The + * second-level keys have no meaning. The second-level values are machine + * names of the grid layouts that the first-level key Paragraphs Type has + * enabled. + * Exception: An empty second-level array means that all layouts are enabled + * for that Paragraphs Type. + */ + protected function getGridLayoutsOrderedByParagraphsTypes() { + if (isset($this->gridLayoutsOrderedByParagraphsTypes)) { + return $this->gridLayoutsOrderedByParagraphsTypes; + } + + $paragraph_type_ids = \Drupal::entityQuery('paragraphs_type')->execute(); + $paragraphs_types_enabled = []; + foreach ($paragraph_type_ids as $paragraph_type_id) { + /** @var ParagraphsType $paragraphs_type */ + $paragraphs_type = ParagraphsType::load($paragraph_type_id); + $configuration = $paragraphs_type->getBehaviorPlugin('grid_layout')->getConfiguration(); + if (isset($configuration['enabled']) && $configuration['enabled']) { + $paragraphs_types_enabled[$paragraph_type_id] = []; + foreach ($configuration['available_grid_layouts'] as $key => $value) { + if ($value) { + $paragraphs_types_enabled[$paragraph_type_id][] = $key; + } + } + } + } + return $this->gridLayoutsOrderedByParagraphsTypes = $paragraphs_types_enabled; + } + + /** + * Lists Paragraphs Types with the style groups they use. + * + * @return array + * An array of style group machine names keyed by Paragraphs Type IDs. Each + * Paragraph Type has the style group is uses as the value. + * Exception: An empty string as the value means that all style groups are + * enabled for that Paragraphs Type. + */ + protected function getStyleGroupsOrderedByParagraphsTypes() { + if (isset($this->styleGroupsOrderedByParagraphsTypes)) { + return $this->styleGroupsOrderedByParagraphsTypes; + } + + $paragraph_type_ids = \Drupal::entityQuery('paragraphs_type')->execute(); + $paragraphs_types_enabled = []; + foreach ($paragraph_type_ids as $paragraph_type_id) { + /** @var ParagraphsType $paragraphs_type */ + $paragraphs_type = ParagraphsType::load($paragraph_type_id); + $configuration = $paragraphs_type->getBehaviorPlugin('style')->getConfiguration(); + if (isset($configuration['enabled']) && $configuration['enabled']) { + $paragraphs_types_enabled[$paragraph_type_id] = $configuration['group']; + } + } + return $this->styleGroupsOrderedByParagraphsTypes = $paragraphs_types_enabled; + } + + /** + * Finds all Paragraphs Types which allow a particular grid layout behaviour. + * + * @param string $layout + * The machine name of the grid layout. + * + * @return array + * Array of IDs of Paragraphs Types that use the grid layout. + */ + public function getParagraphsTypesPerLayout($layout) { + $paragraphs_types = []; + foreach ($this->getGridLayoutsOrderedByParagraphsTypes() as $paragraphs_type => $enabled_layouts) { + if ($enabled_layouts == [] || in_array($layout, $enabled_layouts)) { + $paragraphs_types[] = $paragraphs_type; + } + } + + return $paragraphs_types; + } + + /** + * Finds all Paragraphs Types which allow a particular style behaviour. + * + * @param string $style + * The machine name of the style. + * + * @return array + * Array of IDs of Paragraphs Types that use the style. + */ + public function getParagraphsTypesPerStyle($style) { + $paragraphs_types = []; + foreach ($this->getStyleGroupsOrderedByParagraphsTypes() as $paragraphs_type => $used_style_group) { + $enabled_styles = array_keys($this->styleDiscovery->getStyleOptions($used_style_group)); + if (in_array($style, $enabled_styles)) { + $paragraphs_types[] = $paragraphs_type; + } + } + + return $paragraphs_types; + } + + /** + * Generates an overview page of available layouts for the grid layout plugin. + * + * @return array + * The output render array. + */ + public function layouts() { + $header = [ + 'label' => $this->t('Grid layout'), + 'details' => $this->t('Details'), + 'use' => $this->t('Used in'), + ]; + + $layouts = $this->gridLayoutDiscovery->getGridLayouts(); + uasort($layouts, function ($layout1, $layout2) { + return strcasecmp($layout1['title'], $layout2['title']); + }); + + $rows =[]; + foreach ($layouts as $layout_id => $layout) { + $paragraphs_type_ids = $this->getParagraphsTypesPerLayout($layout_id); + + $paragraphs_type_link_list = []; + foreach ($paragraphs_type_ids as $paragraphs_type_id) { + $paragraphs_type = ParagraphsType::load($paragraphs_type_id); + + if($paragraphs_type_link_list != []) { + $paragraphs_type_link_list[] = ['#plain_text' => ', ']; + } + + $paragraphs_type_link_list[] = [ + '#type' => 'link', + '#title' => $paragraphs_type->label(), + '#url' => $paragraphs_type->toUrl(), + '#attributes' => [ + 'class' => ['table-filter-paragraphs-type-source'], + ], + ]; + } + + $row['label'] = [ + '#type' => 'markup', + '#markup' => Html::escape($layout['title']), + '#prefix' => '', + '#suffix' => '', + ]; + $row['details'] = [ + '#type' => 'details', + '#title' => $this->t($layout['description']) ?: $this->t('Details'), + '#open' => FALSE, + '#attributes' => ['class' => ['overview-details']], + ]; + $row['details']['id'] = [ + '#type' => 'item', + '#title' => $this->t('ID'), + '#markup' => ''. Html::escape($layout_id) . '', + '#prefix' => '
', + '#suffix' => '
', + ]; + $row['use'] = $paragraphs_type_link_list; + + $rows[] = $row; + } + + $table = [ + '#type' => 'table', + '#header' => $header, + '#sticky' => TRUE, + '#attributes' => [ + 'class' => ['paragraphs-collection-overview-table'], + ], + ]; + $table += $rows; + + $filters = [ + '#type' => 'fieldset', + '#attributes' => [ + 'class' => ['table-filter', 'js-show', 'form--inline'], + 'data-table' => '.paragraphs-collection-overview-table', + ], + '#weight' => -10, + '#title' => $this->t('Filter'), + ]; + $filters['text'] = [ + '#type' => 'search', + '#title' => $this->t('Grid layout label or ID'), + '#size' => 40, + '#attributes' => [ + 'class' => ['table-filter-text'], + 'autocomplete' => 'off', + 'title' => $this->t('Enter a part of the style label or ID to filter by.'), + ], + ]; + + $build['table'] = $table; + $build['filters'] = $filters; + $build['#attached']['library'] = ['paragraphs_collection/overview']; + + return $build; + } + + + /** + * Generates an overview page of available styles for the styles plugin. + * + * @return array + * The output render array. + */ + public function styles() { + $header = [ + 'label' => $this->t('Style'), + 'details' => $this->t('Details'), + 'use' => $this->t('Used in'), + ]; + + $styles = $this->styleDiscovery->getStyles(); + uasort($styles, function ($style1, $style2) { + return strcasecmp($style1['title'], $style2['title']); + }); + + $rows =[]; + foreach ($styles as $style_id => $style) { + $paragraphs_type_ids = $this->getParagraphsTypesPerStyle($style_id); + + $paragraphs_type_link_list = []; + foreach ($paragraphs_type_ids as $paragraphs_type_id) { + $paragraphs_type = ParagraphsType::load($paragraphs_type_id); + + if($paragraphs_type_link_list != []) { + $paragraphs_type_link_list[] = ['#plain_text' => ', ']; + } + + $paragraphs_type_link_list[] = [ + '#type' => 'link', + '#title' => $paragraphs_type->label(), + '#url' => $paragraphs_type->toUrl(), + '#attributes' => [ + 'class' => ['table-filter-paragraphs-type-source'], + ], + ]; + } + + $group_list = []; + foreach ($style['groups'] as $group) { + if ($group_list != []) { + $group_list[] = ['#plain_text' => ', ']; + } + $group_list[] = [ + '#type' => 'markup', + '#markup' => Html::escape($group), + '#prefix' => '', + '#suffix' => '', + ]; + } + + $row['label'] = [ + '#type' => 'markup', + '#markup' => Html::escape($style['title']), + '#prefix' => '', + '#suffix' => '', + ]; + $row['details'] = [ + '#type' => 'details', + '#title' => $this->t($style['description']) ?: $this->t('Details'), + '#open' => FALSE, + '#attributes' => ['class' => ['overview-details']], + ]; + $row['details']['id'] = [ + '#type' => 'item', + '#title' => $this->t('ID'), + '#markup' => ''. Html::escape($style_id) . '', + '#prefix' => '
', + '#suffix' => '
', + ]; + $row['details']['groups'] = [ + '#type' => 'item', + '#title' => $this->t('Groups'), + 'item' => $group_list, + '#prefix' => '
', + '#suffix' => '
', + ]; + $row['use'] = $paragraphs_type_link_list; + + $rows[] = $row; + } + + $table = [ + '#type' => 'table', + '#header' => $header, + '#sticky' => TRUE, + '#attributes' => [ + 'class' => ['paragraphs-collection-overview-table'], + ], + ]; + $table += $rows; + + $group_options = $this->styleDiscovery->getStyleGroups(); + asort($group_options); + $empty_option = ['' => '- All -']; + + $filters = [ + '#type' => 'fieldset', + '#attributes' => [ + 'class' => ['table-filter', 'js-show', 'form--inline'], + 'data-table' => '.paragraphs-collection-overview-table', + ], + '#weight' => -10, + '#title' => $this->t('Filter'), + ]; + $filters['group'] = [ + '#type' => 'select', + '#title' => $this->t('Group'), + '#options' => $empty_option + $group_options, + '#attributes' => [ + 'class' => ['table-filter-group-select'], + ], + ]; + $filters['text'] = [ + '#type' => 'search', + '#title' => $this->t('Style label or ID'), + '#size' => 40, + '#attributes' => [ + 'class' => ['table-filter-text'], + 'autocomplete' => 'off', + 'title' => $this->t('Enter a part of the style label or ID to filter by.'), + ], + ]; + + $build['table'] = $table; + $build['filters'] = $filters; + $build['#attached']['library'] = ['paragraphs_collection/overview']; + + return $build; + } + +} diff --git a/src/Tests/ParagraphsCollectionOverviewTest.php b/src/Tests/ParagraphsCollectionOverviewTest.php new file mode 100644 index 0000000..65a2dcb --- /dev/null +++ b/src/Tests/ParagraphsCollectionOverviewTest.php @@ -0,0 +1,87 @@ +loginAsAdmin([ + 'administer paragraphs types', + 'access site reports', + ]); + + // Check the new link on the reports page. + $this->drupalGet('/admin/reports'); + $this->assertText('Overviews of items discoverable by behavior plugins.'); + $this->clickLink('Paragraphs Collection'); + + // Check the grid layouts overview. + $this->assertUrl('/admin/reports/paragraphs_collection/layouts'); + $this->assertTitle('Available grid layouts | Drupal'); + $this->assertText('Grid layout label or ID'); + $this->assertText('Details'); + $this->assertText('Used in'); + + // Check that a concrete grid layout is displayed. + $this->assertText('Two columns'); + $this->assertText('Defines a two column layout with 8-4 widths'); + $this->assertText('paragraphs_two_column'); + $this->assertLink('Grid'); + $this->assertLinkByHref('/admin/structure/paragraphs_type/grid'); + + // Check the tabs. + $this->assertLink('Layouts'); + $this->clickLink('Styles'); + + // Check the styles layouts overview. + $this->assertUrl('/admin/reports/paragraphs_collection/styles'); + $this->assertTitle('Available styles | Drupal'); + $this->assertText('Group'); + $this->assertText('Style label or ID'); + $this->assertText('Details'); + $this->assertText('Used in'); + + // Check that a concrete style is displayed. + $this->assertText('Blue'); + $this->assertText('paragraphs-blue'); + $this->assertText('General Group'); + $this->assertLink('Container'); + $this->assertLinkByHref('/admin/structure/paragraphs_type/paragraphs_container'); + + // Check the tabs. + $this->assertLink('Layouts'); + $this->assertLink('Styles'); + } + +}