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