Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 254 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
ComponentLibraryPanel | |
0.00% |
0 / 248 |
|
0.00% |
0 / 13 |
2352 | |
0.00% |
0 / 1 |
create | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
label | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
defaultConfiguration | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
buildConfigurationForm | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
2 | |||
validateConfigurationForm | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
configurationSummary | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
build | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
30 | |||
getComponentsGrouped | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
12 | |||
getComponentsVariants | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
20 | |||
getComponentsMosaic | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
getDefinitions | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
132 | |||
getProvidersOptions | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getProviders | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Drupal\display_builder\Plugin\display_builder\Island; |
6 | |
7 | use Drupal\Core\Extension\ModuleExtensionList; |
8 | use Drupal\Core\Extension\ThemeExtensionList; |
9 | use Drupal\Core\Form\FormStateInterface; |
10 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
11 | use Drupal\Core\Theme\ThemeManagerInterface; |
12 | use Drupal\Core\Url; |
13 | use Drupal\display_builder\Attribute\Island; |
14 | use Drupal\display_builder\InstanceInterface; |
15 | use Drupal\display_builder\IslandConfigurationFormInterface; |
16 | use Drupal\display_builder\IslandConfigurationFormTrait; |
17 | use Drupal\display_builder\IslandPluginBase; |
18 | use Drupal\display_builder\IslandType; |
19 | use Drupal\ui_patterns\SourcePluginManager; |
20 | use Symfony\Component\DependencyInjection\ContainerInterface; |
21 | |
22 | /** |
23 | * Component library island plugin implementation. |
24 | */ |
25 | #[Island( |
26 | id: 'component_library', |
27 | enabled_by_default: TRUE, |
28 | label: new TranslatableMarkup('Components library'), |
29 | description: new TranslatableMarkup('List of available Components to use.'), |
30 | type: IslandType::Library, |
31 | )] |
32 | class ComponentLibraryPanel extends IslandPluginBase implements IslandConfigurationFormInterface { |
33 | |
34 | use IslandConfigurationFormTrait; |
35 | |
36 | private const HIDE_PROVIDER = ['display_builder', 'sdc_devel']; |
37 | |
38 | /** |
39 | * The module list extension service. |
40 | */ |
41 | protected ThemeManagerInterface $themeManager; |
42 | |
43 | /** |
44 | * The module list extension service. |
45 | */ |
46 | protected ThemeExtensionList $themeList; |
47 | |
48 | /** |
49 | * The module list extension service. |
50 | */ |
51 | protected ModuleExtensionList $moduleList; |
52 | |
53 | /** |
54 | * The UI Patterns source plugin manager. |
55 | */ |
56 | protected SourcePluginManager $sourceManager; |
57 | |
58 | /** |
59 | * The definitions filtered for current theme. |
60 | * |
61 | * @var array |
62 | * The definitions filtered. |
63 | */ |
64 | private array $definitionsFiltered = []; |
65 | |
66 | /** |
67 | * The definitions filtered and grouped for current theme. |
68 | * |
69 | * @var array |
70 | * The definitions filtered and grouped. |
71 | */ |
72 | private array $definitionsGrouped = []; |
73 | |
74 | /** |
75 | * {@inheritdoc} |
76 | */ |
77 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { |
78 | $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); |
79 | $instance->themeManager = $container->get('theme.manager'); |
80 | $instance->themeList = $container->get('extension.list.theme'); |
81 | $instance->moduleList = $container->get('extension.list.module'); |
82 | $instance->sourceManager = $container->get('plugin.manager.ui_patterns_source'); |
83 | |
84 | return $instance; |
85 | } |
86 | |
87 | /** |
88 | * {@inheritdoc} |
89 | */ |
90 | public function label(): string { |
91 | return 'Components'; |
92 | } |
93 | |
94 | /** |
95 | * {@inheritdoc} |
96 | */ |
97 | public function defaultConfiguration(): array { |
98 | return [ |
99 | 'exclude' => [], |
100 | 'status' => [ |
101 | 'experimental', |
102 | ], |
103 | 'include_no_ui' => FALSE, |
104 | 'show_grouped' => TRUE, |
105 | 'show_variants' => TRUE, |
106 | 'show_mosaic' => TRUE, |
107 | ]; |
108 | } |
109 | |
110 | /** |
111 | * {@inheritdoc} |
112 | */ |
113 | public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { |
114 | $configuration = $this->getConfiguration(); |
115 | $components = $this->sdcManager->getDefinitions(); |
116 | |
117 | $form['exclude'] = [ |
118 | '#type' => 'checkboxes', |
119 | '#title' => $this->t('Exclude providers'), |
120 | '#options' => $this->getProvidersOptions($components, $this->t('component'), $this->t('components')), |
121 | '#default_value' => $configuration['exclude'], |
122 | ]; |
123 | |
124 | // @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/schemas/v1/metadata.schema.json#L217 |
125 | $form['status'] = [ |
126 | '#type' => 'checkboxes', |
127 | '#title' => $this->t('Allowed status'), |
128 | '#options' => [ |
129 | 'experimental' => $this->t('Experimental'), |
130 | 'deprecated' => $this->t('Deprecated'), |
131 | 'obsolete' => $this->t('Obsolete'), |
132 | ], |
133 | '#description' => $this->t('Components with stable or undefined status will always be available.'), |
134 | '#default_value' => $configuration['status'], |
135 | ]; |
136 | |
137 | $form['show_grouped'] = [ |
138 | '#type' => 'checkbox', |
139 | '#title' => $this->t('Show components grouped'), |
140 | '#description' => $this->t('Provide a list of grouped components for selection.'), |
141 | '#default_value' => $configuration['show_grouped'], |
142 | ]; |
143 | |
144 | $form['show_variants'] = [ |
145 | '#type' => 'checkbox', |
146 | '#title' => $this->t('Show components variants'), |
147 | '#description' => $this->t('Provide a list of components per variants for selection.'), |
148 | '#default_value' => $configuration['show_variants'], |
149 | ]; |
150 | |
151 | $form['show_mosaic'] = [ |
152 | '#type' => 'checkbox', |
153 | '#title' => $this->t('Show components mosaic'), |
154 | '#description' => $this->t('Provide a list of mosaic components for selection.'), |
155 | '#default_value' => $configuration['show_mosaic'], |
156 | ]; |
157 | |
158 | // Drupal 11.3+ new exclude feature. |
159 | // @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/schemas/v1/metadata.schema.json#L228 |
160 | $form['include_no_ui'] = [ |
161 | '#type' => 'checkbox', |
162 | '#title' => $this->t('Include marked as excluded from the UI'), |
163 | '#description' => $this->t('Components with no ui flag are meant for internal use only. Force to include them. Drupal 11.3+ only.'), |
164 | '#default_value' => $configuration['include_no_ui'], |
165 | ]; |
166 | |
167 | return $form; |
168 | } |
169 | |
170 | /** |
171 | * {@inheritdoc} |
172 | */ |
173 | public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void { |
174 | $values = $form_state->getValues(); |
175 | |
176 | // At least one display must be enabled. |
177 | $show_grouped = (bool) $values['show_grouped']; |
178 | $show_variants = (bool) $values['show_variants']; |
179 | $show_mosaic = (bool) $values['show_mosaic']; |
180 | |
181 | if (!$show_grouped && !$show_variants && !$show_mosaic) { |
182 | $form_state->setError($form['show_grouped'], $this->t('At least one display must be selected!')); |
183 | $form_state->setError($form['show_variants'], $this->t('At least one display must be selected!')); |
184 | $form_state->setError($form['show_mosaic'], $this->t('At least one display must be selected!')); |
185 | } |
186 | } |
187 | |
188 | /** |
189 | * {@inheritdoc} |
190 | */ |
191 | public function configurationSummary(): array { |
192 | $configuration = $this->getConfiguration(); |
193 | |
194 | $summary = []; |
195 | |
196 | $summary[] = $this->t('Excluded providers: @exclude', [ |
197 | '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'), |
198 | ]); |
199 | |
200 | $summary[] = $this->t('Allowed status: @status', [ |
201 | '@status' => \implode(', ', \array_filter(\array_unique(\array_merge(['stable', 'undefined'], $configuration['status'] ?? []))) ?: [$this->t('stable, undefined')]), |
202 | ]); |
203 | |
204 | $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components'); |
205 | |
206 | $list = []; |
207 | |
208 | if ((bool) $configuration['show_grouped']) { |
209 | $list[] = $this->t('grouped'); |
210 | } |
211 | |
212 | if ((bool) $configuration['show_variants']) { |
213 | $list[] = $this->t('variants'); |
214 | } |
215 | |
216 | if ((bool) $configuration['show_mosaic']) { |
217 | $list[] = $this->t('mosaic'); |
218 | } |
219 | $summary[] = $this->t('Components list as: @list', [ |
220 | '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'), |
221 | ]); |
222 | |
223 | return $summary; |
224 | } |
225 | |
226 | /** |
227 | * {@inheritdoc} |
228 | */ |
229 | public function build(InstanceInterface $builder, array $data = [], array $options = []): array { |
230 | $builder_id = (string) $builder->id(); |
231 | // Try to call only once for each sub islands. |
232 | $definitions = $this->getDefinitions(); |
233 | $configuration = $this->getConfiguration(); |
234 | |
235 | $this->definitionsFiltered = $definitions['filtered'] ?? []; |
236 | $this->definitionsGrouped = $definitions['grouped'] ?? []; |
237 | |
238 | $panes = []; |
239 | |
240 | if ((bool) $configuration['show_grouped']) { |
241 | $panes['grouped'] = [ |
242 | 'title' => $this->t('Grouped'), |
243 | 'content' => $this->getComponentsGrouped($builder_id), |
244 | ]; |
245 | } |
246 | |
247 | if ((bool) $configuration['show_variants']) { |
248 | $panes['variants'] = [ |
249 | 'title' => $this->t('Variants'), |
250 | 'content' => $this->getComponentsVariants($builder_id), |
251 | ]; |
252 | } |
253 | |
254 | if ((bool) $configuration['show_mosaic']) { |
255 | $panes['mosaic'] = [ |
256 | 'title' => $this->t('Mosaic'), |
257 | 'content' => $this->getComponentsMosaic($builder_id), |
258 | ]; |
259 | } |
260 | |
261 | $tabs = []; |
262 | $content = []; |
263 | |
264 | foreach ($panes as $pane_id => $pane) { |
265 | $id = 'db-' . $builder_id . '-components-tab---' . $pane_id; |
266 | $tabs[] = [ |
267 | 'title' => $pane['title'], |
268 | 'url' => '#' . $id, |
269 | ]; |
270 | $content[] = $this->wrapContent($pane['content'], $id); |
271 | } |
272 | |
273 | return [ |
274 | '#type' => 'component', |
275 | '#component' => 'display_builder:library_panel', |
276 | '#slots' => [ |
277 | 'tabs' => $this->buildTabs('db-' . $builder_id . '-components-tabs', $tabs), |
278 | 'content' => $content, |
279 | ], |
280 | ]; |
281 | } |
282 | |
283 | /** |
284 | * Gets the grouped components view. |
285 | * |
286 | * @param string $builder_id |
287 | * Builder ID. |
288 | * |
289 | * @return array |
290 | * A renderable array containing the grouped components. |
291 | */ |
292 | private function getComponentsGrouped(string $builder_id): array { |
293 | $build = []; |
294 | |
295 | /** @var \Drupal\ui_patterns\SourceWithChoicesInterface $source */ |
296 | $source = $this->sourceManager->createInstance('component'); |
297 | |
298 | foreach ($this->definitionsGrouped as $group_name => $group) { |
299 | $build[] = [ |
300 | '#type' => 'html_tag', |
301 | '#tag' => 'h4', |
302 | '#value' => $group_name, |
303 | '#attributes' => [ |
304 | 'class' => ['db-filter-hide-on-search'], |
305 | ], |
306 | ]; |
307 | |
308 | foreach ($group as $component_id => $definition) { |
309 | $component_id = (string) $component_id; |
310 | $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]); |
311 | |
312 | $data = [ |
313 | 'source_id' => 'component', |
314 | 'source' => $source->getChoiceSettings($component_id), |
315 | ]; |
316 | // Used for search filter. |
317 | $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']); |
318 | $build[] = $this->buildPlaceholderButtonWithPreview($builder_id, $definition['annotated_name'], $data, $component_preview_url, $keywords); |
319 | } |
320 | } |
321 | |
322 | return $this->buildDraggables($builder_id, $build); |
323 | } |
324 | |
325 | /** |
326 | * Gets the components variants view. |
327 | * |
328 | * @param string $builder_id |
329 | * Builder ID. |
330 | * |
331 | * @return array |
332 | * A renderable array containing the variants placeholders. |
333 | */ |
334 | private function getComponentsVariants(string $builder_id): array { |
335 | $build = []; |
336 | |
337 | /** @var \Drupal\ui_patterns\SourceWithChoicesInterface $source */ |
338 | $source = $this->sourceManager->createInstance('component'); |
339 | |
340 | foreach ($this->definitionsFiltered as $component_id => $definition) { |
341 | $build[] = [ |
342 | '#type' => 'html_tag', |
343 | '#tag' => 'h4', |
344 | '#value' => $definition['annotated_name'], |
345 | '#attributes' => [ |
346 | 'data-filter-parent' => $definition['machineName'], |
347 | ], |
348 | ]; |
349 | |
350 | $data = [ |
351 | 'source_id' => 'component', |
352 | 'source' => $source->getChoiceSettings($component_id), |
353 | ]; |
354 | |
355 | if (!isset($definition['variants'])) { |
356 | $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]); |
357 | // Used for search filter. |
358 | $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']); |
359 | $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords); |
360 | $build_variant['#attributes']['data-filter-child'] = $definition['machineName']; |
361 | $build[] = $build_variant; |
362 | |
363 | continue; |
364 | } |
365 | |
366 | foreach ($definition['variants'] ?? [] as $variant_id => $variant) { |
367 | $params = ['component_id' => $component_id, 'variant_id' => $variant_id]; |
368 | $component_preview_url = Url::fromRoute('display_builder.api_component_preview', $params); |
369 | $data['source']['component']['variant_id'] = [ |
370 | 'source_id' => 'select', |
371 | 'source' => [ |
372 | 'value' => $variant_id, |
373 | ], |
374 | ]; |
375 | // Used for search filter. |
376 | $keywords = \sprintf('%s %s %s', $definition['label'], $variant['title'], $definition['provider']); |
377 | $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $variant['title'], $data, $component_preview_url, $keywords); |
378 | $build_variant['#attributes']['data-filter-child'] = $definition['machineName']; |
379 | $build[] = $build_variant; |
380 | } |
381 | } |
382 | |
383 | return $this->buildDraggables($builder_id, $build); |
384 | } |
385 | |
386 | /** |
387 | * Gets the mosaic view of components. |
388 | * |
389 | * @param string $builder_id |
390 | * Builder ID. |
391 | * |
392 | * @return array |
393 | * A renderable array containing the mosaic view of components. |
394 | */ |
395 | private function getComponentsMosaic(string $builder_id): array { |
396 | $components = []; |
397 | |
398 | /** @var \Drupal\ui_patterns\SourceInterface $source */ |
399 | $source = $this->sourceManager->createInstance('component'); |
400 | |
401 | foreach (\array_keys($this->definitionsFiltered) as $component_id) { |
402 | $component_id = (string) $component_id; |
403 | $component = $this->sdcManager->find($component_id); |
404 | $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]); |
405 | |
406 | /** @var \Drupal\ui_patterns\SourceWithChoicesInterface $source */ |
407 | $vals = [ |
408 | 'source_id' => 'component', |
409 | 'source' => $source->getChoiceSettings($component_id), |
410 | ]; |
411 | $thumbnail = $component->metadata->getThumbnailPath(); |
412 | |
413 | // Used for search filter. |
414 | $keywords = \sprintf('%s %s', $component->metadata->name, \str_replace(':', ' ', $component_id)); |
415 | $build = $this->buildPlaceholderCardWithPreview($component->metadata->name, $vals, $component_preview_url, $keywords, $thumbnail); |
416 | $components[] = $build; |
417 | } |
418 | |
419 | return $this->buildDraggables($builder_id, $components, 'mosaic'); |
420 | } |
421 | |
422 | /** |
423 | * Get filtered and grouped definitions. |
424 | * |
425 | * @return array |
426 | * The definitions filtered and grouped. |
427 | */ |
428 | private function getDefinitions(): array { |
429 | $definitions = $this->sdcManager->getSortedDefinitions(); |
430 | $configuration = $this->getConfiguration(); |
431 | |
432 | $filtered_definitions = $grouped_definitions = []; |
433 | |
434 | foreach ($definitions as $id => $definition) { |
435 | if (isset($definition['provider']) && \in_array($definition['provider'], self::HIDE_PROVIDER, TRUE)) { |
436 | continue; |
437 | } |
438 | |
439 | if (isset($definition['provider']) && \in_array($definition['provider'], $configuration['exclude'], TRUE)) { |
440 | continue; |
441 | } |
442 | |
443 | // Excluded no ui components unless forced. |
444 | if (isset($definition['noUi']) && $definition['noUi'] === TRUE) { |
445 | if ((bool) $configuration['include_no_ui'] !== TRUE) { |
446 | continue; |
447 | } |
448 | } |
449 | |
450 | // Filter components according to configuration. |
451 | // Components with stable or undefined status will always be available. |
452 | $allowed_status = \array_merge($configuration['status'], ['stable']); |
453 | |
454 | if (isset($definition['status']) && !\in_array($definition['status'], $allowed_status, TRUE)) { |
455 | continue; |
456 | } |
457 | $allowed_status = \array_merge($configuration['status'], ['stable']); |
458 | |
459 | $filtered_definitions[$id] = $definition; |
460 | $grouped_definitions[(string) $definition['category']][$id] = $definition; |
461 | } |
462 | |
463 | // Order list ignoring starting '(' that is used for components names that |
464 | // are sub components. |
465 | \uasort($filtered_definitions, static function ($a, $b) { |
466 | $nameA = \ltrim($a['name'] ?? $a['label'], '('); |
467 | |
468 | return \strnatcasecmp($nameA, \ltrim($b['name'] ?? $b['label'], '(')); |
469 | }); |
470 | |
471 | return [ |
472 | 'grouped' => $grouped_definitions, |
473 | 'filtered' => $filtered_definitions, |
474 | ]; |
475 | } |
476 | |
477 | /** |
478 | * Get providers options for select input. |
479 | * |
480 | * @param array $definitions |
481 | * Plugin definitions. |
482 | * @param string|TranslatableMarkup $singular |
483 | * Singular label of the plugins. |
484 | * @param string|TranslatableMarkup $plural |
485 | * Plural label of the plugins. |
486 | * |
487 | * @return array |
488 | * An associative array with extension ID as key and extension description |
489 | * as value. |
490 | */ |
491 | private function getProvidersOptions(array $definitions, string|TranslatableMarkup $singular = 'definition', string|TranslatableMarkup $plural = 'definitions'): array { |
492 | $options = []; |
493 | |
494 | foreach ($this->getProviders($definitions) as $provider_id => $provider) { |
495 | $params = [ |
496 | '@name' => $provider['name'], |
497 | '@type' => $provider['type'], |
498 | '@count' => $provider['count'], |
499 | '@singular' => $singular, |
500 | '@plural' => $plural, |
501 | ]; |
502 | $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@type, @count @singular)', '@name (@type, @count @plural)', $params); |
503 | } |
504 | |
505 | return $options; |
506 | } |
507 | |
508 | /** |
509 | * Get all providers. |
510 | * |
511 | * @param array $definitions |
512 | * Plugin definitions. |
513 | * |
514 | * @return array |
515 | * Drupal extension definitions, keyed by extension ID |
516 | */ |
517 | protected function getProviders(array $definitions): array { |
518 | $themes = $this->themeList->getAllInstalledInfo(); |
519 | $modules = $this->moduleList->getAllInstalledInfo(); |
520 | $providers = []; |
521 | |
522 | foreach ($definitions as $definition) { |
523 | $provider_id = $definition['provider']; |
524 | |
525 | if (\in_array($provider_id, self::HIDE_PROVIDER, TRUE)) { |
526 | continue; |
527 | } |
528 | $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL; |
529 | |
530 | if (!$provider) { |
531 | continue; |
532 | } |
533 | $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1; |
534 | $providers[$provider_id] = $provider; |
535 | } |
536 | |
537 | return $providers; |
538 | } |
539 | } |