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