Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 182 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
BlockLibraryPanel | |
0.00% |
0 / 176 |
|
0.00% |
0 / 14 |
3540 | |
0.00% |
0 / 1 |
create | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
defaultConfiguration | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
buildConfigurationForm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
configurationSummary | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
build | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 | |||
label | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getChoiceGroupLabel | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
110 | |||
getGroupedChoices | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
sortGroupedChoices | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
56 | |||
getSources | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
isChoiceValid | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
56 | |||
getChoices | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
42 | |||
getProvidersOptions | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getProviders | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Drupal\display_builder\Plugin\display_builder\Island; |
6 | |
7 | use Drupal\Component\Render\MarkupInterface; |
8 | use Drupal\Core\Extension\ModuleExtensionList; |
9 | use Drupal\Core\Form\FormStateInterface; |
10 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
11 | use Drupal\display_builder\Attribute\Island; |
12 | use Drupal\display_builder\InstanceInterface; |
13 | use Drupal\display_builder\IslandConfigurationFormInterface; |
14 | use Drupal\display_builder\IslandConfigurationFormTrait; |
15 | use Drupal\display_builder\IslandPluginBase; |
16 | use Drupal\display_builder\IslandType; |
17 | use Drupal\ui_patterns\SourcePluginBase; |
18 | use Drupal\ui_patterns\SourcePluginManager; |
19 | use Drupal\ui_patterns\SourceWithChoicesInterface; |
20 | use 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 Drupal blocks to use.'), |
30 | type: IslandType::Library, |
31 | )] |
32 | class BlockLibraryPanel extends IslandPluginBase implements IslandConfigurationFormInterface { |
33 | |
34 | use IslandConfigurationFormTrait; |
35 | |
36 | private const HIDE_BLOCK = [ |
37 | 'help_block', |
38 | 'system_messages_block', |
39 | 'htmx_loader', |
40 | 'broken', |
41 | 'system_main_block', |
42 | 'page_title_block', |
43 | ]; |
44 | |
45 | private const HIDE_SOURCE = [ |
46 | 'component', |
47 | // Used only for imports from Manage Display and Layout Builder. |
48 | 'extra_field', |
49 | ]; |
50 | |
51 | private const HIDE_PROVIDER = ['ui_patterns_blocks']; |
52 | |
53 | /** |
54 | * The sources. |
55 | */ |
56 | protected ?array $sources = NULL; |
57 | |
58 | /** |
59 | * The choices from all sources. |
60 | */ |
61 | protected ?array $choices = NULL; |
62 | |
63 | /** |
64 | * The module list extension service. |
65 | */ |
66 | protected ModuleExtensionList $moduleList; |
67 | |
68 | /** |
69 | * The UI Patterns source plugin manager. |
70 | */ |
71 | protected SourcePluginManager $sourceManager; |
72 | |
73 | /** |
74 | * {@inheritdoc} |
75 | */ |
76 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { |
77 | $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); |
78 | $instance->moduleList = $container->get('extension.list.module'); |
79 | $instance->sourceManager = $container->get('plugin.manager.ui_patterns_source'); |
80 | |
81 | return $instance; |
82 | } |
83 | |
84 | /** |
85 | * {@inheritdoc} |
86 | */ |
87 | public function defaultConfiguration(): array { |
88 | return [ |
89 | 'exclude' => [ |
90 | 'devel', |
91 | 'htmx', |
92 | 'shortcut', |
93 | ], |
94 | ]; |
95 | } |
96 | |
97 | /** |
98 | * {@inheritdoc} |
99 | */ |
100 | public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { |
101 | $configuration = $this->getConfiguration(); |
102 | |
103 | $form['exclude'] = [ |
104 | '#type' => 'checkboxes', |
105 | '#title' => $this->t('Exclude modules'), |
106 | '#options' => $this->getProvidersOptions(), |
107 | '#default_value' => $configuration['exclude'], |
108 | ]; |
109 | |
110 | return $form; |
111 | } |
112 | |
113 | /** |
114 | * {@inheritdoc} |
115 | */ |
116 | public function configurationSummary(): array { |
117 | $configuration = $this->getConfiguration(); |
118 | |
119 | return [ |
120 | $this->t('Excluded modules: @exclude', [ |
121 | '@exclude' => \implode(', ', \array_filter($configuration['exclude'] ?? []) ?: [$this->t('None')]), |
122 | ]), |
123 | ]; |
124 | } |
125 | |
126 | /** |
127 | * {@inheritdoc} |
128 | */ |
129 | public function build(InstanceInterface $builder, array $data = [], array $options = []): array { |
130 | $builder_id = (string) $builder->id(); |
131 | $categories = $this->getGroupedChoices(); |
132 | $build = []; |
133 | |
134 | foreach ($categories as $category_data) { |
135 | if (!empty($category_data['label'])) { |
136 | $build[] = [ |
137 | [ |
138 | '#type' => 'html_tag', |
139 | '#tag' => 'h4', |
140 | // We hide the group titles on search. |
141 | '#attributes' => ['class' => 'db-filter-hide-on-search'], |
142 | '#value' => $category_data['label'], |
143 | ], |
144 | ]; |
145 | } |
146 | $category_choices = $category_data['choices']; |
147 | |
148 | foreach ($category_choices as $choice) { |
149 | $build[] = $this->buildPlaceholderButton( |
150 | $choice['label'], |
151 | $choice['data'] ?? [], |
152 | $choice['keywords'] ?? '' |
153 | ); |
154 | } |
155 | } |
156 | |
157 | return $this->buildDraggables($builder_id, $build); |
158 | } |
159 | |
160 | /** |
161 | * {@inheritdoc} |
162 | */ |
163 | public function label(): string { |
164 | return 'Blocks'; |
165 | } |
166 | |
167 | /** |
168 | * Get the group label for a choice. |
169 | * |
170 | * @param array $choice |
171 | * The choice to get the group for. |
172 | * @param array $source_definition |
173 | * The source definition to use for the group. |
174 | * |
175 | * @return string |
176 | * The group label for the choice. |
177 | */ |
178 | private static function getChoiceGroupLabel(array &$choice, array &$source_definition): string { |
179 | $group = $source_definition['label'] ?? ''; |
180 | |
181 | switch ($source_definition['id']) { |
182 | case 'block': |
183 | $block_id = $choice['original_id'] ?? ''; |
184 | |
185 | if (\str_starts_with($block_id, 'views_block:') && $choice['group']) { |
186 | $group = $choice['group']; |
187 | } |
188 | elseif (\str_starts_with($block_id, 'system_menu_block:') && $choice['group']) { |
189 | $group = $choice['group']; |
190 | } |
191 | else { |
192 | $group = new TranslatableMarkup('Others'); |
193 | } |
194 | |
195 | break; |
196 | |
197 | case 'entity_reference': |
198 | $group = new TranslatableMarkup('Referenced entities'); |
199 | |
200 | break; |
201 | |
202 | case 'entity_field': |
203 | $group = new TranslatableMarkup('Fields'); |
204 | |
205 | break; |
206 | |
207 | default: |
208 | break; |
209 | } |
210 | |
211 | return ($group instanceof MarkupInterface) ? (string) $group : $group; |
212 | } |
213 | |
214 | /** |
215 | * Get the choices grouped by category. |
216 | * |
217 | * @return array |
218 | * An array of grouped choices. |
219 | */ |
220 | private function getGroupedChoices(): array { |
221 | $choices = $this->getChoices(); |
222 | $categories = []; |
223 | |
224 | foreach ($choices as $choice) { |
225 | $category = $choice['group'] ?? ''; |
226 | |
227 | if ($category instanceof MarkupInterface) { |
228 | $category = (string) $category; |
229 | } |
230 | |
231 | if (!isset($categories[$category])) { |
232 | $categories[$category] = [ |
233 | 'label' => $category, |
234 | 'metadata' => $choice, |
235 | 'choices' => [], |
236 | ]; |
237 | } |
238 | $categories[$category]['choices'][] = $choice; |
239 | } |
240 | self::sortGroupedChoices($categories); |
241 | |
242 | return $categories; |
243 | } |
244 | |
245 | /** |
246 | * Sorts the grouped choices. |
247 | * |
248 | * This method sorts the categories by their labels, placing empty category |
249 | * first, views blocks are sorted to the end of the list. |
250 | * |
251 | * @param array $categories |
252 | * The categories to sort, passed by reference. |
253 | */ |
254 | private static function sortGroupedChoices(array &$categories): void { |
255 | // Sort categories : empty first, views at the end. |
256 | \usort($categories, static function ($a, $b) { |
257 | if (empty($a['label'])) { |
258 | return -1; |
259 | } |
260 | |
261 | if (empty($b['label'])) { |
262 | return 1; |
263 | } |
264 | $source_id_a = $a['metadata']['data']['source_id'] ?? ''; |
265 | $source_id_b = $b['metadata']['data']['source_id'] ?? ''; |
266 | |
267 | if (($source_id_a === 'block') && ($source_id_b !== 'block')) { |
268 | return 1; |
269 | } |
270 | |
271 | if (($source_id_b === 'block') && ($source_id_a !== 'block')) { |
272 | return -1; |
273 | } |
274 | |
275 | return \strnatcmp($a['label'], $b['label']); |
276 | }); |
277 | } |
278 | |
279 | /** |
280 | * Returns all possible sources. |
281 | * |
282 | * @throws \Drupal\Component\Plugin\Exception\PluginException |
283 | * |
284 | * @return array<string, array> |
285 | * An array of sources. |
286 | */ |
287 | private function getSources(): array { |
288 | if ($this->sources === NULL) { |
289 | $definitions = $this->sourceManager->getDefinitionsForPropType('slot', $this->configuration['contexts'] ?? []); |
290 | $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]]; |
291 | |
292 | foreach ($definitions as $source_id => $definition) { |
293 | if (\in_array($source_id, self::HIDE_SOURCE, TRUE)) { |
294 | continue; |
295 | } |
296 | $source = $this->sourceManager->createInstance($source_id, |
297 | SourcePluginBase::buildConfiguration('slot', $slot_definition, ['source' => []], $this->configuration['contexts'] ?? []) |
298 | ); |
299 | $this->sources[$source_id] = [ |
300 | 'definition' => $definition, |
301 | 'source' => $source, |
302 | ]; |
303 | |
304 | if ($source instanceof SourceWithChoicesInterface) { |
305 | $this->sources[$source_id]['choices'] = $source->getChoices(); |
306 | } |
307 | } |
308 | } |
309 | |
310 | return $this->sources; |
311 | } |
312 | |
313 | /** |
314 | * Validate a choice against the source definition and allowed providers. |
315 | * |
316 | * @param array $choice |
317 | * The choice to validate. |
318 | * @param array $source_definition |
319 | * The source definition. |
320 | * @param array $excluded_providers |
321 | * The excluded providers. |
322 | * |
323 | * @return bool |
324 | * Whether the choice is valid or not. |
325 | */ |
326 | private function isChoiceValid(array &$choice, array &$source_definition, array $excluded_providers = []): bool { |
327 | $provider = $choice['provider'] ?? ''; |
328 | |
329 | if ($provider) { |
330 | if (\in_array($provider, self::HIDE_PROVIDER, TRUE) && \in_array($provider, $excluded_providers, TRUE)) { |
331 | return FALSE; |
332 | } |
333 | } |
334 | |
335 | if ($source_definition['id'] === 'block') { |
336 | $block_id = $choice['original_id'] ?? ''; |
337 | |
338 | if ($block_id && \in_array($block_id, self::HIDE_BLOCK, TRUE)) { |
339 | return FALSE; |
340 | } |
341 | } |
342 | |
343 | return TRUE; |
344 | } |
345 | |
346 | /** |
347 | * Get the choices from all sources. |
348 | * |
349 | * @return array |
350 | * An array of choices. |
351 | */ |
352 | private function getChoices(): array { |
353 | if ($this->choices !== NULL) { |
354 | return $this->choices; |
355 | } |
356 | |
357 | $this->choices = []; |
358 | |
359 | $configuration = $this->getConfiguration(); |
360 | $excluded_providers = $configuration['exclude'] ?? []; |
361 | $sources = $this->getSources(); |
362 | |
363 | foreach ($sources as $source_id => $source_data) { |
364 | $definition = $source_data['definition']; |
365 | $source = $source_data['source']; |
366 | |
367 | if (!isset($source_data['choices'])) { |
368 | $this->choices[] = [ |
369 | 'label' => $definition['label'] ?? $source_id, |
370 | 'data' => ['source_id' => $source_id], |
371 | 'keywords' => \sprintf('%s %s %s', $definition['id'], $definition['label'] ?? $source_id, $definition['description'] ?? ''), |
372 | ]; |
373 | |
374 | continue; |
375 | } |
376 | $choices = $source_data['choices']; |
377 | |
378 | foreach ($choices as $choice_id => $choice) { |
379 | if (!$this->isChoiceValid($choice, $definition, $excluded_providers)) { |
380 | continue; |
381 | } |
382 | $choice_label = $choice['label'] ?? $choice_id; |
383 | $group_label = self::getChoiceGroupLabel($choice, $definition); |
384 | $this->choices[] = [ |
385 | 'group' => $group_label, |
386 | 'label' => $choice_label, |
387 | 'data' => [ |
388 | 'source_id' => $source_id, |
389 | 'source' => $source->getChoiceSettings($choice_id), |
390 | ], |
391 | 'keywords' => \sprintf('%s %s %s %s', $definition['id'], $choice_label, $definition['description'] ?? '', $choice_id), |
392 | ]; |
393 | } |
394 | } |
395 | |
396 | return $this->choices; |
397 | } |
398 | |
399 | /** |
400 | * Get providers options for select input. |
401 | * |
402 | * @return array |
403 | * An associative array with module ID as key and module description as |
404 | * value. |
405 | */ |
406 | private function getProvidersOptions(): array { |
407 | $options = []; |
408 | |
409 | foreach ($this->getProviders() as $provider_id => $provider) { |
410 | $params = [ |
411 | '@name' => $provider['name'], |
412 | '@count' => $provider['count'], |
413 | ]; |
414 | $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@count block)', '@name (@count blocks)', $params); |
415 | } |
416 | |
417 | return $options; |
418 | } |
419 | |
420 | /** |
421 | * Get all providers. |
422 | * |
423 | * @return array |
424 | * Drupal modules definitions, keyed by extension ID |
425 | */ |
426 | private function getProviders(): array { |
427 | $sources = $this->getSources(); |
428 | $providers = []; |
429 | $modules = $this->moduleList->getAllInstalledInfo(); |
430 | |
431 | foreach ($sources as $source_data) { |
432 | if (!isset($source_data['choices'])) { |
433 | continue; |
434 | } |
435 | $choices = $source_data['choices']; |
436 | |
437 | foreach ($choices as $choice) { |
438 | $provider = $choice['provider'] ?? ''; |
439 | |
440 | if (!$provider || \in_array($provider, self::HIDE_PROVIDER, TRUE)) { |
441 | continue; |
442 | } |
443 | |
444 | if (!isset($modules[$provider])) { |
445 | // If the provider is not a module, skip it. |
446 | continue; |
447 | } |
448 | |
449 | if (!isset($providers[$provider])) { |
450 | $providers[$provider] = $modules[$provider]; |
451 | $providers[$provider]['count'] = 0; |
452 | } |
453 | ++$providers[$provider]['count']; |
454 | } |
455 | } |
456 | |
457 | return $providers; |
458 | } |
459 | |
460 | } |