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

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

ComponentLibraryPanel->build
242  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
243    $builder_id = (string) $builder->id();
244    // Run a single time and saved as properties to avoid repeating processing
245    // in ::getComponentsMosaic(), ::getComponentsVariants() and
246    // ::getComponentsGrouped().
247    $configuration = $this->getConfiguration();
248
249    $componentDefinitions = new ComponentLibraryDefinitionHelper($this->sdcManager, $this->sourceManager);
250    $definitions = $componentDefinitions->getDefinitions($configuration);
251
252    $this->definitionsFiltered = $definitions['filtered'] ?? [];
253    $this->definitionsGrouped = $definitions['grouped'] ?? [];
254    $this->sourcesData = $definitions['sources'] ?? [];
255
256    $panes = [];
257
258    if ((bool) $configuration['show_grouped']) {
260        'title' => $this->t('Grouped'),
261        'content' => $this->getComponentsGrouped($builder_id),
262      ];
263    }
264
265    if ((bool) $configuration['show_variants']) {
265    if ((bool) $configuration['show_variants']) {
267        'title' => $this->t('Variants'),
268        'content' => $this->getComponentsVariants($builder_id),
269      ];
270    }
271
272    if ((bool) $configuration['show_mosaic']) {
272    if ((bool) $configuration['show_mosaic']) {
274        'title' => $this->t('Mosaic'),
275        'content' => $this->getComponentsMosaic($builder_id),
276      ];
277    }
278
279    $tabs = [];
279    $tabs = [];
280    $content = [];
281
282    foreach ($panes as $pane_id => $pane) {
282    foreach ($panes as $pane_id => $pane) {
282    foreach ($panes as $pane_id => $pane) {
282    foreach ($panes as $pane_id => $pane) {
283      $id = 'db-' . $builder_id . '-components-tab---' . $pane_id;
284      $tabs[] = [
285        'title' => $pane['title'],
286        'url' => '#' . $id,
287      ];
288      $content[] = $this->wrapContent($pane['content'], $id);
289    }
290
291    return [
292      '#type' => 'component',
293      '#component' => 'display_builder:library_panel',
294      '#slots' => [
295        'tabs' => (\count($panes) > 1) ? $this->buildTabs('db-' . $builder_id . '-components-tabs', $tabs) : [],
295        'tabs' => (\count($panes) > 1) ? $this->buildTabs('db-' . $builder_id . '-components-tabs', $tabs) : [],
292      '#type' => 'component',
292      '#type' => 'component',
293      '#component' => 'display_builder:library_panel',
294      '#slots' => [
295        'tabs' => (\count($panes) > 1) ? $this->buildTabs('db-' . $builder_id . '-components-tabs', $tabs) : [],
296        'content' => $content,
297      ],
298    ];
299  }
ComponentLibraryPanel->buildConfigurationForm
107  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
108    $configuration = $this->getConfiguration();
109    $components = $this->sdcManager->getDefinitions();
110
111    $form['exclude'] = [
112      '#type' => 'checkboxes',
113      '#title' => $this->t('Exclude providers'),
114      '#options' => $this->getProvidersOptions($components, $this->t('component'), $this->t('components')),
115      '#default_value' => $configuration['exclude'],
116    ];
117
118    $form['exclude_id'] = [
119      '#type' => 'textarea',
120      '#title' => $this->t('Exclude by id'),
121      '#description' => $this->t('Provide a space separated list of components id to exclude, must be prefixed by provider. Example: "ui_suite_bootstrap:card_body<br>ui_suite_bootstrap:table_cell".'),
122      '#default_value' => $configuration['exclude_id'],
123    ];
124
125    // @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/schemas/v1/metadata.schema.json#L217
126    $form['component_status'] = [
127      '#type' => 'checkboxes',
128      '#title' => $this->t('Allowed status'),
129      '#options' => [
130        'experimental' => $this->t('Experimental'),
131        'deprecated' => $this->t('Deprecated'),
132        'obsolete' => $this->t('Obsolete'),
133      ],
134      '#description' => $this->t('Components with stable or undefined status will always be available.'),
135      '#default_value' => $configuration['component_status'],
136    ];
137
138    $form['show_grouped'] = [
139      '#type' => 'checkbox',
140      '#title' => $this->t('Show components grouped'),
141      '#description' => $this->t('Provide a list of grouped components for selection.'),
142      '#default_value' => $configuration['show_grouped'],
143    ];
144
145    $form['show_variants'] = [
146      '#type' => 'checkbox',
147      '#title' => $this->t('Show components variants'),
148      '#description' => $this->t('Provide a list of components per variants for selection.'),
149      '#default_value' => $configuration['show_variants'],
150    ];
151
152    $form['show_mosaic'] = [
153      '#type' => 'checkbox',
154      '#title' => $this->t('Show components mosaic'),
155      '#description' => $this->t('Provide a list of mosaic components for selection.'),
156      '#default_value' => $configuration['show_mosaic'],
157    ];
158
159    // Drupal 11.3+ new exclude feature.
160    // @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/schemas/v1/metadata.schema.json#L228
161    $form['include_no_ui'] = [
162      '#type' => 'checkbox',
163      '#title' => $this->t('Include marked as excluded from the UI'),
164      '#description' => $this->t('Components with no ui flag are meant for internal use only. Force to include them. Drupal 11.3+ only.'),
165      '#default_value' => $configuration['include_no_ui'],
166    ];
167
168    return $form;
169  }
ComponentLibraryPanel->configurationSummary
193    $configuration = $this->getConfiguration();
194
195    $summary = [];
196
197    $summary[] = $this->t('Excluded providers: @exclude', [
198      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
198      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
198      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
198      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
199    ]);
200
201    if (\strlen($configuration['exclude_id'] ?? '') > 5) {
202      $value = \preg_split('/\s+/', \trim($configuration['exclude_id'] ?? ''));
203
204      if ($value === FALSE) {
204      if ($value === FALSE) {
205        $summary[] = $this->t('Component(s) excluded');
208        $num = \count($value);
209        $summary[] = $this->formatPlural($num, '@count component excluded', '@count components excluded');
210      }
211    }
212
213    $summary[] = $this->t('Allowed status: @status', [
213    $summary[] = $this->t('Allowed status: @status', [
214      '@status' => \implode(', ', \array_filter(\array_unique(\array_merge(['stable', 'undefined'], $configuration['component_status'] ?? []))) ?: [$this->t('stable, undefined')]),
215    ]);
216
217    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
217    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
217    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
217    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
218
219    $list = [];
220
221    if ((bool) $configuration['show_grouped']) {
222      $list[] = $this->t('grouped');
223    }
224
225    if ((bool) $configuration['show_variants']) {
225    if ((bool) $configuration['show_variants']) {
226      $list[] = $this->t('variants');
227    }
228
229    if ((bool) $configuration['show_mosaic']) {
229    if ((bool) $configuration['show_mosaic']) {
230      $list[] = $this->t('mosaic');
231    }
232    $summary[] = $this->t('Components list as: @list', [
232    $summary[] = $this->t('Components list as: @list', [
233      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
233      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
233      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
233      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
234    ]);
235
236    return $summary;
237  }
ComponentLibraryPanel->create
78  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
79    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
80    $instance->themeManager = $container->get('theme.manager');
81    $instance->themeList = $container->get('extension.list.theme');
82    $instance->moduleList = $container->get('extension.list.module');
83
84    return $instance;
85  }
ComponentLibraryPanel->defaultConfiguration
92      'exclude' => [],
93      'exclude_id' => '',
94      'component_status' => [
95        'experimental',
96      ],
97      'include_no_ui' => FALSE,
98      'show_grouped' => TRUE,
99      'show_variants' => TRUE,
100      'show_mosaic' => TRUE,
101    ];
102  }
ComponentLibraryPanel->getComponentsGrouped
339  private function getComponentsGrouped(string $builder_id): array {
340    $build = [];
341
342    foreach ($this->definitionsGrouped as $group_name => $group) {
342    foreach ($this->definitionsGrouped as $group_name => $group) {
342    foreach ($this->definitionsGrouped as $group_name => $group) {
343      $build[] = [
344        '#type' => 'html_tag',
345        '#tag' => 'h4',
346        '#value' => $group_name,
347        '#attributes' => [
348          'class' => ['db-filter-hide-on-search'],
349        ],
350      ];
351
352      foreach ($group as $component_id => $definition) {
352      foreach ($group as $component_id => $definition) {
352      foreach ($group as $component_id => $definition) {
342    foreach ($this->definitionsGrouped as $group_name => $group) {
343      $build[] = [
344        '#type' => 'html_tag',
345        '#tag' => 'h4',
346        '#value' => $group_name,
347        '#attributes' => [
348          'class' => ['db-filter-hide-on-search'],
349        ],
350      ];
351
352      foreach ($group as $component_id => $definition) {
342    foreach ($this->definitionsGrouped as $group_name => $group) {
343      $build[] = [
344        '#type' => 'html_tag',
345        '#tag' => 'h4',
346        '#value' => $group_name,
347        '#attributes' => [
348          'class' => ['db-filter-hide-on-search'],
349        ],
350      ];
351
352      foreach ($group as $component_id => $definition) {
353        $component_id = (string) $component_id;
354        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
355
356        $data = [
357          'source_id' => 'component',
358          'source' => $this->sourcesData[$component_id],
359        ];
360        // Used for search filter.
361        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
362        $build[] = $this->buildPlaceholderButtonWithPreview($builder_id, $definition['annotated_name'], $data, $component_preview_url, $keywords);
363      }
364    }
365
366    return $this->buildDraggables($builder_id, $build);
367  }
ComponentLibraryPanel->getComponentsMosaic
450  private function getComponentsMosaic(string $builder_id): array {
451    $components = [];
452
453    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
453    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
453    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
454      $component_id = (string) $component_id;
453    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
454      $component_id = (string) $component_id;
455      $component = $this->sdcManager->find($component_id);
456      $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
457
458      $vals = [
459        'source_id' => 'component',
460        'source' => $this->sourcesData[$component_id],
461      ];
462      $thumbnail = $component->metadata->getThumbnailPath();
463
464      // Used for search filter.
465      $keywords = \sprintf('%s %s', $component->metadata->name, \str_replace(':', ' ', $component_id));
466      $build = $this->buildPlaceholderCardWithPreview($component->metadata->name, $vals, $component_preview_url, $keywords, $thumbnail);
467      // Label is used by default to set drawer title when dragging. It is set
468      // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
469      // to override it to have the proper label and not the variant name.
470      // @see assets/js/db_drawer.js
471      // @see src/RenderableBuilderTrait::buildPlaceholderButton()
472      $build['#attributes']['data-node-title'] = $component->metadata->name;
473      $components[] = $build;
474    }
475
476    return $this->buildDraggables($builder_id, $components, 'mosaic');
477  }
ComponentLibraryPanel->getComponentsVariants
378  private function getComponentsVariants(string $builder_id): array {
379    $build = [];
380
381    foreach ($this->definitionsFiltered as $component_id => $definition) {
381    foreach ($this->definitionsFiltered as $component_id => $definition) {
381    foreach ($this->definitionsFiltered as $component_id => $definition) {
382      $build[] = [
383        '#type' => 'html_tag',
384        '#tag' => 'h4',
385        '#value' => $definition['annotated_name'],
386        '#attributes' => [
387          'data-search-section' => $definition['machineName'],
388        ],
389      ];
390
391      $data = [
392        'source_id' => 'component',
393        'source' => $this->sourcesData[$component_id],
394      ];
395
396      if (!isset($definition['variants'])) {
397        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
398        // Used for search filter.
399        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
400        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords);
401        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
402        // Label is used by default to set drawer title when dragging. It is set
403        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
404        // to override it to have the proper label and not the variant name.
405        // @see assets/js/db_drawer.js
406        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
407        $build_variant['#attributes']['data-node-title'] = $definition['label'];
408
409        $build[] = $build_variant;
410
411        continue;
414      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
414      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
414      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
381    foreach ($this->definitionsFiltered as $component_id => $definition) {
382      $build[] = [
383        '#type' => 'html_tag',
384        '#tag' => 'h4',
385        '#value' => $definition['annotated_name'],
386        '#attributes' => [
387          'data-search-section' => $definition['machineName'],
388        ],
389      ];
390
391      $data = [
392        'source_id' => 'component',
393        'source' => $this->sourcesData[$component_id],
394      ];
395
396      if (!isset($definition['variants'])) {
397        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
398        // Used for search filter.
399        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
400        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords);
401        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
402        // Label is used by default to set drawer title when dragging. It is set
403        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
404        // to override it to have the proper label and not the variant name.
405        // @see assets/js/db_drawer.js
406        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
407        $build_variant['#attributes']['data-node-title'] = $definition['label'];
408
409        $build[] = $build_variant;
410
411        continue;
412      }
413
414      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
381    foreach ($this->definitionsFiltered as $component_id => $definition) {
382      $build[] = [
383        '#type' => 'html_tag',
384        '#tag' => 'h4',
385        '#value' => $definition['annotated_name'],
386        '#attributes' => [
387          'data-search-section' => $definition['machineName'],
388        ],
389      ];
390
391      $data = [
392        'source_id' => 'component',
393        'source' => $this->sourcesData[$component_id],
394      ];
395
396      if (!isset($definition['variants'])) {
397        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
398        // Used for search filter.
399        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
400        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords);
401        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
402        // Label is used by default to set drawer title when dragging. It is set
403        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
404        // to override it to have the proper label and not the variant name.
405        // @see assets/js/db_drawer.js
406        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
407        $build_variant['#attributes']['data-node-title'] = $definition['label'];
408
409        $build[] = $build_variant;
410
411        continue;
412      }
413
414      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
415        $params = ['component_id' => $component_id, 'variant_id' => $variant_id];
416        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', $params);
417        $data['source']['component']['variant_id'] = [
418          'source_id' => 'select',
419          'source' => [
420            'value' => $variant_id,
421          ],
422        ];
423        // Used for search filter.
424        $keywords = \sprintf('%s %s %s', $definition['label'], $variant['title'], $definition['provider']);
425        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $variant['title'], $data, $component_preview_url, $keywords);
426        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
427        // Label is used by default to set drawer title when dragging. It is set
428        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
429        // to override it to have the proper label and not the variant name.
430        // @see assets/js/db_drawer.js
431        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
432        $build_variant['#attributes']['data-node-title'] = $definition['label'];
433
434        $build[] = $build_variant;
435      }
436    }
437
438    return $this->buildDraggables($builder_id, $build);
439  }
ComponentLibraryPanel->getProviders
310  protected function getProviders(array $definitions): array {
311    $themes = $this->themeList->getAllInstalledInfo();
312    $modules = $this->moduleList->getAllInstalledInfo();
313    $providers = [];
314
315    foreach ($definitions as $definition) {
315    foreach ($definitions as $definition) {
316      $provider_id = $definition['provider'];
317
318      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
319
320      if (!$provider) {
321        continue;
323      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
323      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
323      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
315    foreach ($definitions as $definition) {
316      $provider_id = $definition['provider'];
317
318      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
319
320      if (!$provider) {
321        continue;
322      }
323      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
315    foreach ($definitions as $definition) {
316      $provider_id = $definition['provider'];
317
318      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
319
320      if (!$provider) {
321        continue;
322      }
323      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
324      $providers[$provider_id] = $provider;
325    }
326
327    return $providers;
328  }
ComponentLibraryPanel->getProvidersOptions
493  private function getProvidersOptions(array $definitions, string|TranslatableMarkup $singular = 'definition', string|TranslatableMarkup $plural = 'definitions'): array {
494    $options = [];
495
496    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
496    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
496    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
496    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
497      $params = [
498        '@name' => $provider['name'],
499        '@type' => $provider['type'],
500        '@count' => $provider['count'],
501        '@singular' => $singular,
502        '@plural' => $plural,
503      ];
504      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@type, @count @singular)', '@name (@type, @count @plural)', $params);
505    }
506
507    return $options;
508  }
ComponentLibraryPanel->validateConfigurationForm
174  public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
175    $values = $form_state->getValues();
176
177    // At least one display must be enabled.
178    $show_grouped = (bool) $values['show_grouped'];
179    $show_variants = (bool) $values['show_variants'];
180    $show_mosaic = (bool) $values['show_mosaic'];
181
182    if (!$show_grouped && !$show_variants && !$show_mosaic) {
182    if (!$show_grouped && !$show_variants && !$show_mosaic) {
182    if (!$show_grouped && !$show_variants && !$show_mosaic) {
182    if (!$show_grouped && !$show_variants && !$show_mosaic) {
182    if (!$show_grouped && !$show_variants && !$show_mosaic) {
183      $form_state->setError($form['show_grouped'], $this->t('At least one display must be selected!'));
184      $form_state->setError($form['show_variants'], $this->t('At least one display must be selected!'));
185      $form_state->setError($form['show_mosaic'], $this->t('At least one display must be selected!'));
186    }
187  }
187  }