diff --git a/css/overview.admin.css b/css/overview.admin.css new file mode 100644 index 0000000..f53c610 --- /dev/null +++ b/css/overview.admin.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.admin.js b/js/overview.admin.js new file mode 100644 index 0000000..81a8cf6 --- /dev/null +++ b/js/overview.admin.js @@ -0,0 +1,66 @@ +/** + * @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 $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 e4d793f..95217dc 100644 --- a/paragraphs_collection.libraries.yml +++ b/paragraphs_collection.libraries.yml @@ -1,3 +1,14 @@ +overview: + css: + component: + css/overview.admin.css: {} + js: + js/overview.admin.js: {} + dependencies: + - core/drupal + - core/jquery + - core/jquery.once + paragraphs_quote: 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..8ebda6b --- /dev/null +++ b/src/Controller/OverviewController.php @@ -0,0 +1,456 @@ + [ + * 'grid_pt' => $grid_paragraphs_type_object, + * ] + * ] + * @endcode + * + * @var array + */ + protected $paragraphsTypesGroupedByGridLayouts; + + /** + * A nested array of Paragraphs Type objects. + * + * A nested array. The first level is keyed by style machine names. The second + * level is keyed Paragraphs Type IDs. The second-level values are Paragraphs + * Type objects that allow the respective grid layout. Styles are ordered by + * name. + * + * Example: + * @code + * [ + * 'blue_style' => [ + * 'style_pt' => $style_paragraphs_type_object, + * ] + * ] + * @endcode + * + * @var array + */ + protected $paragraphsTypesGroupedByStyles; + + /** + * Constructs a \Drupal\paragraphs_collection\Controller\OverviewController object. + * + * @param \Drupal\paragraphs_collection\GridLayoutDiscoveryInterface $grid_layout_discovery + * The discovery service for grid layout files. + * @param \Drupal\paragraphs_collection\StyleDiscoveryInterface $style_discovery + * The discovery service for style files. + */ + public function __construct(GridLayoutDiscoveryInterface $grid_layout_discovery, StyleDiscoveryInterface $style_discovery) { + $this->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 grid layouts with the Paragraphs Types that allow them. + * + * @return array + * A nested array. The first level is keyed by grid layout machine names. + * The second level is keyed Paragraphs Type IDs. The second-level values + * are Paragraphs Type objects that allow the respective grid layout. Grid + * layouts are ordered by name. + * Example: + * @code + * [ + * '1_2_1_column_layout' => [ + * 'grid_pt' => $grid_paragraphs_type_object, + * ] + * ] + * @endcode + */ + public function getParagraphsTypesGroupedByGridLayouts() { + if (isset($this->paragraphsTypesGroupedByGridLayouts)) { + return $this->paragraphsTypesGroupedByGridLayouts; + } + + $paragraph_type_ids = \Drupal::entityQuery('paragraphs_type')->execute(); + $paragraphs_types = ParagraphsType::loadMultiple($paragraph_type_ids); + + // Find all enabled grid layouts for each Paragraphs Type. + // An empty array as the second-level value means that all grid layouts are + // enabled for that Paragraphs type. + $grid_layouts_grouped_by_paragraphs_types = []; + foreach ($paragraphs_types as $paragraph_type_id => $paragraphs_type) { + /** @var ParagraphsType $paragraphs_type */ + $configuration = $paragraphs_type->getBehaviorPlugin('grid_layout')->getConfiguration(); + if (isset($configuration['enabled']) && $configuration['enabled']) { + $grid_layouts_grouped_by_paragraphs_types[$paragraph_type_id] = []; + foreach ($configuration['available_grid_layouts'] as $key => $value) { + if ($value) { + $grid_layouts_grouped_by_paragraphs_types[$paragraph_type_id][] = $key; + } + } + } + } + + //Get all grid layouts ordered by title. + $layouts = $this->gridLayoutDiscovery->getGridLayouts(); + uasort($layouts, function ($layout1, $layout2) { + return strcasecmp($layout1['title'], $layout2['title']); + }); + + // Group Paragraphs Types by grid layouts. + $paragraphs_types_grouped_by_grid_layouts = []; + foreach ($layouts as $layout_id => $layout) { + foreach ($grid_layouts_grouped_by_paragraphs_types as $paragraphs_type_id => $enabled_layouts) { + if ($enabled_layouts == [] || in_array($layout_id, $enabled_layouts)) { + $paragraphs_types_grouped_by_grid_layouts[$layout_id][$paragraphs_type_id] = $paragraphs_types[$paragraphs_type_id]; + } + } + } + + return $this->paragraphsTypesGroupedByGridLayouts = $paragraphs_types_grouped_by_grid_layouts; + } + + /** + * Lists styles with the Paragraphs Types that allow them. + * + * @return array + * A nested array. The first level is keyed by style machine names. The + * second level is keyed Paragraphs Type IDs. The second-level values are + * Paragraphs Type objects that allow the respective grid layout. Styles + * are ordered by name. + * Example: + * @code + * [ + * 'blue_style' => [ + * 'style_pt' => $style_paragraphs_type_object, + * ] + * ] + * @endcode + */ + public function getParagraphsTypesGroupedByStyles() { + if (isset($this->paragraphsTypesGroupedByStyles)) { + return $this->paragraphsTypesGroupedByStyles; + } + + $paragraph_type_ids = \Drupal::entityQuery('paragraphs_type')->execute(); + $paragraphs_types = ParagraphsType::loadMultiple($paragraph_type_ids); + + // Find the used style group for each Paragraphs Type. + // An as empty string as the second-level value means that the Paragraphs + // Type uses all style groups. + $styles_grouped_by_paragraphs_types = []; + foreach ($paragraphs_types as $paragraph_type_id => $paragraphs_type) { + /** @var ParagraphsType $paragraphs_type */ + $configuration = $paragraphs_type->getBehaviorPlugin('style')->getConfiguration(); + if (isset($configuration['enabled']) && $configuration['enabled']) { + $styles_grouped_by_paragraphs_types[$paragraph_type_id] = $configuration['group']; + } + } + + //Get all styles ordered by title. + $styles = $this->styleDiscovery->getStyles(); + uasort($styles, function ($style1, $style2) { + return strcasecmp($style1['title'], $style2['title']); + }); + + // Group Paragraphs Types by styles. + $paragraphs_types_grouped_by_styles = []; + foreach ($styles as $style_id => $style) { + foreach ($styles_grouped_by_paragraphs_types as $paragraphs_type_id => $used_style_group) { + $enabled_styles = array_keys($this->styleDiscovery->getStyleOptions($used_style_group)); + if (in_array($style_id, $enabled_styles)) { + $paragraphs_types_grouped_by_styles[$style_id][$paragraphs_type_id] = $paragraphs_types[$paragraphs_type_id]; + } + } + } + + return $this->paragraphsTypesGroupedByStyles = $paragraphs_types_grouped_by_styles; + } + + /** + * Generates an overview page of available layouts for the grid layout plugin. + * + * @return array + * The output render array. + */ + public function layouts() { + $filters = [ + '#type' => 'fieldset', + '#attributes' => [ + 'class' => ['table-filter', 'js-show', 'form--inline'], + 'data-table' => '.paragraphs-collection-overview-table', + ], + '#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.'), + ], + ]; + + $header = [ + 'label' => $this->t('Grid layout'), + 'details' => $this->t('Details'), + 'use' => $this->t('Used in'), + ]; + + $layouts = $this->gridLayoutDiscovery->getGridLayouts(); + + $rows = []; + foreach ($this->getParagraphsTypesGroupedByGridLayouts() as $layout_id => $value) { + $layout = $layouts[$layout_id]; + + $paragraphs_type_link_list = []; + foreach ($value as $paragraphs_type_id => $paragraphs_type) { + /** @var ParagraphsType $paragraphs_type */ + + 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' => 'container', + '#plain_text' => $layout['title'], + '#attributes' => ['class' => ['table-filter-text-source']], + ]; + $row['details'] = [ + '#type' => 'details', + '#title' => $layout['description'] ?: $this->t('Description not available.'), + '#open' => FALSE, + '#attributes' => ['class' => ['overview-details']], + ]; + $row['details']['id'] = [ + '#type' => 'item', + '#title' => $this->t('ID'), + '#prefix' => '', + '#suffix' => '', + 'item' => [ + '#type' => 'container', + '#plain_text' => $layout_id, + '#attributes' => ['class' => ['table-filter-text-source']], + ], + ]; + $row['use'] = $paragraphs_type_link_list; + + $rows[] = $row; + } + + $table = [ + '#type' => 'table', + '#header' => $header, + '#sticky' => TRUE, + '#attributes' => [ + 'class' => ['paragraphs-collection-overview-table'], + ], + ]; + $table += $rows; + + $build['filters'] = $filters; + $build['table'] = $table; + $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() { + $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', + ], + '#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.'), + ], + ]; + + $header = [ + 'label' => $this->t('Style'), + 'details' => $this->t('Details'), + 'use' => $this->t('Used in'), + ]; + + $styles = $this->styleDiscovery->getStyles(); + + $rows = []; + foreach ($this->getParagraphsTypesGroupedByStyles() as $style_id => $value) { + $style = $styles[$style_id]; + + $paragraphs_type_link_list = []; + foreach ($value as $paragraphs_type_id => $paragraphs_type) { + /** @var ParagraphsType $paragraphs_type */ + + 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' => 'container', + '#plain_text' => $group, + '#attributes' => ['class' => ['table-filter-group-source']], + ]; + } + + $row['label'] = [ + '#type' => 'container', + '#plain_text' => $style['title'], + '#attributes' => ['class' => ['table-filter-text-source']], + ]; + $row['details'] = [ + '#type' => 'details', + '#title' => $style['description'] ?: $this->t('Description not available.'), + '#open' => FALSE, + '#attributes' => ['class' => ['overview-details']], + ]; + $row['details']['id'] = [ + '#type' => 'item', + '#title' => $this->t('ID'), + '#prefix' => '', + '#suffix' => '', + 'item' => [ + '#type' => 'container', + '#plain_text' => $style_id, + '#attributes' => ['class' => ['table-filter-text-source']], + ], + ]; + $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; + + $build['filters'] = $filters; + $build['table'] = $table; + $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'); + } + +}