Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockLibraryPanel
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 9
812
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 defaultConfiguration
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildConfigurationForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 configurationSummary
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 build
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 buildCategorySection
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getSources
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 getProvidersOptions
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getProviders
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Extension\ModuleExtensionList;
8use Drupal\Core\Form\FormStateInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\BlockLibrarySourceHelper;
12use Drupal\display_builder\InstanceInterface;
13use Drupal\display_builder\Island\IslandConfigurationFormInterface;
14use Drupal\display_builder\Island\IslandConfigurationFormTrait;
15use Drupal\display_builder\Island\IslandPluginBase;
16use Drupal\display_builder\Island\IslandType;
17use Drupal\display_builder\SourceWithSlotsInterface;
18use Drupal\ui_patterns\SourcePluginBase;
19use Drupal\ui_patterns\SourceWithChoicesInterface;
20use Symfony\Component\DependencyInjection\ContainerInterface;
21
22/**
23 * Block library island plugin implementation.
24 */
25#[Island(
26  id: 'block_library',
27  enabled_by_default: TRUE,
28  label: new TranslatableMarkup('Blocks'),
29  description: new TranslatableMarkup('List of available blocks.'),
30  type: IslandType::Library,
31)]
32class BlockLibraryPanel extends IslandPluginBase implements IslandConfigurationFormInterface {
33
34  use IslandConfigurationFormTrait;
35
36  private const HIDE_SOURCE = [
37    'component',
38    // @todo enable Wysiwyg from our UI when #3561474 is fixed.
39    'wysiwyg',
40  ];
41
42  private const HIDE_PROVIDER = ['ui_patterns_blocks'];
43
44  /**
45   * The sources.
46   */
47  protected array $sources = [];
48
49  /**
50   * The module extension list service.
51   */
52  protected ModuleExtensionList $moduleList;
53
54  /**
55   * {@inheritdoc}
56   */
57  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
58    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
59    $instance->moduleList = $container->get('extension.list.module');
60
61    return $instance;
62  }
63
64  /**
65   * {@inheritdoc}
66   */
67  public function defaultConfiguration(): array {
68    return [
69      'exclude' => [
70        'devel',
71        'htmx',
72        'shortcut',
73      ],
74    ];
75  }
76
77  /**
78   * {@inheritdoc}
79   */
80  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
81    $configuration = $this->getConfiguration();
82
83    $form['exclude'] = [
84      '#type' => 'checkboxes',
85      '#title' => $this->t('Exclude modules'),
86      '#options' => $this->getProvidersOptions(),
87      '#default_value' => $configuration['exclude'],
88    ];
89
90    return $form;
91  }
92
93  /**
94   * {@inheritdoc}
95   */
96  public function configurationSummary(): array {
97    $configuration = $this->getConfiguration();
98
99    return [
100      $this->t('Excluded modules: @exclude', [
101        '@exclude' => \implode(', ', \array_filter($configuration['exclude'] ?? []) ?: [$this->t('None')]),
102      ]),
103    ];
104  }
105
106  /**
107   * {@inheritdoc}
108   */
109  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
110    $builder_id = (string) $builder->id();
111    $configuration = $this->getConfiguration();
112
113    $exclude_providers = \array_merge(
114      $configuration['exclude'] ?? [],
115      self::HIDE_PROVIDER
116    );
117    $categories = BlockLibrarySourceHelper::getGroupedChoices(
118      $this->getSources(),
119      $exclude_providers,
120    );
121
122    $build = [];
123
124    foreach ($categories as $category_data) {
125      $build[] = $this->buildCategorySection($category_data, $builder_id);
126    }
127
128    return [
129      '#type' => 'component',
130      '#component' => 'display_builder:library_panel',
131      '#slots' => [
132        'content' => $this->buildDraggables($builder_id, $build),
133      ],
134    ];
135  }
136
137  /**
138   * Build a category section.
139   *
140   * @param array $category_data
141   *   The category data.
142   * @param string $builder_id
143   *   The builder ID.
144   *
145   * @return array
146   *   The render array for the category section.
147   */
148  private function buildCategorySection(array $category_data, string $builder_id): array {
149    $section = [];
150
151    if (!empty($category_data['label'])) {
152      $section[] = [
153        '#type' => 'html_tag',
154        '#tag' => 'h4',
155        '#attributes' => ['class' => 'db-filter-hide-on-search'],
156        '#value' => $category_data['label'],
157      ];
158    }
159
160    foreach ($category_data['choices'] as $choice) {
161      $section[] = $choice['preview']
162        ? $this->buildPlaceholderButtonWithPreview($builder_id, $choice['label'], $choice['data'] ?? [], $choice['preview'], $choice['keywords'] ?? '')
163        : $this->buildPlaceholderButton($choice['label'], $choice['data'] ?? [], $choice['keywords'] ?? '');
164    }
165
166    return $section;
167  }
168
169  /**
170   * Returns all possible sources.
171   *
172   * @throws \Drupal\Component\Plugin\Exception\PluginException
173   *
174   * @return array<string, array>
175   *   An array of sources.
176   */
177  private function getSources(): array {
178    if (!empty($this->sources)) {
179      return $this->sources;
180    }
181
182    $definitions = $this->sourceManager->getDefinitionsForPropType('slot', $this->configuration['contexts'] ?? []);
183    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
184
185    foreach ($definitions as $source_id => $definition) {
186      // A block is a source for slots but without slots.
187      if (\is_a($definition['class'], SourceWithSlotsInterface::class, TRUE)) {
188        continue;
189      }
190
191      if (\in_array($source_id, self::HIDE_SOURCE, TRUE)) {
192        continue;
193      }
194
195      try {
196        $source = $this->sourceManager->createInstance(
197          $source_id,
198          SourcePluginBase::buildConfiguration('slot', $slot_definition, ['source' => []], $this->configuration['contexts'] ?? [])
199        );
200      }
201      catch (\Throwable $e) {
202        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
203
204        continue;
205      }
206
207      $this->sources[$source_id] = [
208        'definition' => $definition,
209        'source' => $source,
210      ];
211
212      if ($source instanceof SourceWithChoicesInterface) {
213        $this->sources[$source_id]['choices'] = $source->getChoices();
214      }
215    }
216
217    return $this->sources;
218  }
219
220  /**
221   * Get providers options for select input.
222   *
223   * @return array
224   *   An associative array with module ID as key and module description as
225   *   value.
226   */
227  private function getProvidersOptions(): array {
228    $options = [];
229
230    foreach ($this->getProviders() as $provider_id => $provider) {
231      $params = [
232        '@name' => $provider['name'],
233        '@count' => $provider['count'],
234      ];
235      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@count block)', '@name (@count blocks)', $params);
236    }
237
238    return $options;
239  }
240
241  /**
242   * Get all providers.
243   *
244   * @return array
245   *   Drupal modules definitions, keyed by extension ID
246   */
247  private function getProviders(): array {
248    $sources = $this->getSources();
249    $providers = [];
250    $modules = $this->moduleList->getAllInstalledInfo();
251
252    foreach ($sources as $source_data) {
253      if (!isset($source_data['choices'])) {
254        continue;
255      }
256      $choices = $source_data['choices'];
257
258      foreach ($choices as $choice) {
259        $provider = $choice['provider'] ?? '';
260
261        if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) {
262          continue;
263        }
264
265        if (!isset($modules[$provider])) {
266          // If the provider is not a module, skip it.
267          continue;
268        }
269
270        if (!isset($providers[$provider])) {
271          $providers[$provider] = $modules[$provider];
272          $providers[$provider]['count'] = 0;
273        }
274        ++$providers[$provider]['count'];
275      }
276    }
277
278    return $providers;
279  }
280
281}