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

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
256  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
257    $builder_id = (string) $builder->id();
258    // Run a single time and saved as properties to avoid repeating processing
259    // in ::getComponentsMosaic(), ::getComponentsVariants() and
260    // ::getComponentsGrouped().
261    $configuration = $this->getConfiguration();
262
263    $componentDefinitions = new ComponentLibraryDefinitionHelper($this->sdcManager, $this->sourceManager);
264    $definitions = $componentDefinitions->getDefinitions($configuration);
265
266    $this->definitionsFiltered = $definitions['filtered'] ?? [];
267    $this->definitionsGrouped = $definitions['grouped'] ?? [];
268    $this->sourcesData = $definitions['sources'] ?? [];
269
270    $panes = [];
271
272    if ((bool) $configuration['show_grouped']) {
274        'title' => $this->t('Grouped'),
275        'content' => $this->getComponentsGrouped($builder_id),
276      ];
277    }
278
279    if ((bool) $configuration['show_variants']) {
279    if ((bool) $configuration['show_variants']) {
281        'title' => $this->t('Variants'),
282        'content' => $this->getComponentsVariants($builder_id),
283      ];
284    }
285
286    if ((bool) $configuration['show_mosaic']) {
286    if ((bool) $configuration['show_mosaic']) {
288        'title' => $this->t('Mosaic'),
289        'content' => $this->getComponentsMosaic($builder_id),
290      ];
291    }
292
293    $tabs = [];
293    $tabs = [];
294    $content = [];
295
296    foreach ($panes as $pane_id => $pane) {
296    foreach ($panes as $pane_id => $pane) {
296    foreach ($panes as $pane_id => $pane) {
296    foreach ($panes as $pane_id => $pane) {
297      $id = 'db-' . $builder_id . '-components-tab---' . $pane_id;
298      $tabs[] = [
299        'title' => $pane['title'],
300        'url' => '#' . $id,
301      ];
302      $content[] = $this->wrapContent($pane['content'], $id);
303    }
304
305    return [
306      '#type' => 'component',
307      '#component' => 'display_builder:library_panel',
308      '#slots' => [
309        'tabs' => $this->buildTabs('db-' . $builder_id . '-components-tabs', $tabs),
310        'content' => $content,
ComponentLibraryPanel->buildConfigurationForm
121  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
122    $configuration = $this->getConfiguration();
123    $components = $this->sdcManager->getDefinitions();
124
125    $form['exclude'] = [
126      '#type' => 'checkboxes',
127      '#title' => $this->t('Exclude providers'),
128      '#options' => $this->getProvidersOptions($components, $this->t('component'), $this->t('components')),
129      '#default_value' => $configuration['exclude'],
130    ];
131
132    $form['exclude_id'] = [
133      '#type' => 'textarea',
134      '#title' => $this->t('Exclude by id'),
135      '#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".'),
136      '#default_value' => $configuration['exclude_id'],
137    ];
138
139    // @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/schemas/v1/metadata.schema.json#L217
140    $form['component_status'] = [
141      '#type' => 'checkboxes',
142      '#title' => $this->t('Allowed status'),
143      '#options' => [
144        'experimental' => $this->t('Experimental'),
145        'deprecated' => $this->t('Deprecated'),
146        'obsolete' => $this->t('Obsolete'),
147      ],
148      '#description' => $this->t('Components with stable or undefined status will always be available.'),
149      '#default_value' => $configuration['component_status'],
150    ];
151
152    $form['show_grouped'] = [
153      '#type' => 'checkbox',
154      '#title' => $this->t('Show components grouped'),
155      '#description' => $this->t('Provide a list of grouped components for selection.'),
156      '#default_value' => $configuration['show_grouped'],
157    ];
158
159    $form['show_variants'] = [
160      '#type' => 'checkbox',
161      '#title' => $this->t('Show components variants'),
162      '#description' => $this->t('Provide a list of components per variants for selection.'),
163      '#default_value' => $configuration['show_variants'],
164    ];
165
166    $form['show_mosaic'] = [
167      '#type' => 'checkbox',
168      '#title' => $this->t('Show components mosaic'),
169      '#description' => $this->t('Provide a list of mosaic components for selection.'),
170      '#default_value' => $configuration['show_mosaic'],
171    ];
172
173    // Drupal 11.3+ new exclude feature.
174    // @see https://git.drupalcode.org/project/drupal/-/blob/11.x/core/assets/schemas/v1/metadata.schema.json#L228
175    $form['include_no_ui'] = [
176      '#type' => 'checkbox',
177      '#title' => $this->t('Include marked as excluded from the UI'),
178      '#description' => $this->t('Components with no ui flag are meant for internal use only. Force to include them. Drupal 11.3+ only.'),
179      '#default_value' => $configuration['include_no_ui'],
180    ];
181
182    return $form;
ComponentLibraryPanel->configurationSummary
207    $configuration = $this->getConfiguration();
208
209    $summary = [];
210
211    $summary[] = $this->t('Excluded providers: @exclude', [
212      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
212      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
212      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
212      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
213    ]);
214
215    if (\strlen($configuration['exclude_id'] ?? '') > 5) {
216      $value = \preg_split('/\s+/', \trim($configuration['exclude_id'] ?? ''));
217
218      if ($value === FALSE) {
218      if ($value === FALSE) {
219        $summary[] = $this->t('Component(s) excluded');
222        $num = \count($value);
223        $summary[] = $this->formatPlural($num, '@count component excluded', '@count components excluded');
224      }
225    }
226
227    $summary[] = $this->t('Allowed status: @status', [
227    $summary[] = $this->t('Allowed status: @status', [
228      '@status' => \implode(', ', \array_filter(\array_unique(\array_merge(['stable', 'undefined'], $configuration['component_status'] ?? []))) ?: [$this->t('stable, undefined')]),
229    ]);
230
231    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
231    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
231    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
231    $summary[] = $configuration['include_no_ui'] ? $this->t('Include no UI components') : $this->t('Exclude no UI components');
232
233    $list = [];
234
235    if ((bool) $configuration['show_grouped']) {
236      $list[] = $this->t('grouped');
237    }
238
239    if ((bool) $configuration['show_variants']) {
239    if ((bool) $configuration['show_variants']) {
240      $list[] = $this->t('variants');
241    }
242
243    if ((bool) $configuration['show_mosaic']) {
243    if ((bool) $configuration['show_mosaic']) {
244      $list[] = $this->t('mosaic');
245    }
246    $summary[] = $this->t('Components list as: @list', [
246    $summary[] = $this->t('Components list as: @list', [
247      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
247      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
247      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
247      '@list' => !empty($list) ? \implode(', ', $list) : $this->t('None selected'),
248    ]);
249
250    return $summary;
ComponentLibraryPanel->create
84  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
85    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
86    $instance->themeManager = $container->get('theme.manager');
87    $instance->themeList = $container->get('extension.list.theme');
88    $instance->moduleList = $container->get('extension.list.module');
89    $instance->sourceManager = $container->get('plugin.manager.ui_patterns_source');
90
91    return $instance;
ComponentLibraryPanel->defaultConfiguration
106      'exclude' => [],
ComponentLibraryPanel->getComponentsGrouped
353  private function getComponentsGrouped(string $builder_id): array {
354    $build = [];
355
356    foreach ($this->definitionsGrouped as $group_name => $group) {
356    foreach ($this->definitionsGrouped as $group_name => $group) {
356    foreach ($this->definitionsGrouped as $group_name => $group) {
357      $build[] = [
358        '#type' => 'html_tag',
359        '#tag' => 'h4',
360        '#value' => $group_name,
361        '#attributes' => [
362          'class' => ['db-filter-hide-on-search'],
363        ],
364      ];
365
366      foreach ($group as $component_id => $definition) {
366      foreach ($group as $component_id => $definition) {
366      foreach ($group as $component_id => $definition) {
356    foreach ($this->definitionsGrouped as $group_name => $group) {
357      $build[] = [
358        '#type' => 'html_tag',
359        '#tag' => 'h4',
360        '#value' => $group_name,
361        '#attributes' => [
362          'class' => ['db-filter-hide-on-search'],
363        ],
364      ];
365
366      foreach ($group as $component_id => $definition) {
356    foreach ($this->definitionsGrouped as $group_name => $group) {
357      $build[] = [
358        '#type' => 'html_tag',
359        '#tag' => 'h4',
360        '#value' => $group_name,
361        '#attributes' => [
362          'class' => ['db-filter-hide-on-search'],
363        ],
364      ];
365
366      foreach ($group as $component_id => $definition) {
367        $component_id = (string) $component_id;
368        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
369
370        $data = [
371          'source_id' => 'component',
372          'source' => $this->sourcesData[$component_id],
373        ];
374        // Used for search filter.
375        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
376        $build[] = $this->buildPlaceholderButtonWithPreview($builder_id, $definition['annotated_name'], $data, $component_preview_url, $keywords);
377      }
378    }
379
380    return $this->buildDraggables($builder_id, $build);
ComponentLibraryPanel->getComponentsMosaic
464  private function getComponentsMosaic(string $builder_id): array {
465    $components = [];
466
467    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
467    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
467    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
468      $component_id = (string) $component_id;
467    foreach (\array_keys($this->definitionsFiltered) as $component_id) {
468      $component_id = (string) $component_id;
469      $component = $this->sdcManager->find($component_id);
470      $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
471
472      $vals = [
473        'source_id' => 'component',
474        'source' => $this->sourcesData[$component_id],
475      ];
476      $thumbnail = $component->metadata->getThumbnailPath();
477
478      // Used for search filter.
479      $keywords = \sprintf('%s %s', $component->metadata->name, \str_replace(':', ' ', $component_id));
480      $build = $this->buildPlaceholderCardWithPreview($component->metadata->name, $vals, $component_preview_url, $keywords, $thumbnail);
481      // Label is used by default to set drawer title when dragging. It is set
482      // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
483      // to override it to have the proper label and not the variant name.
484      // @see assets/js/db_drawer.js
485      // @see src/RenderableBuilderTrait::buildPlaceholderButton()
486      $build['#attributes']['data-node-title'] = $component->metadata->name;
487      $components[] = $build;
488    }
489
490    return $this->buildDraggables($builder_id, $components, 'mosaic');
ComponentLibraryPanel->getComponentsVariants
392  private function getComponentsVariants(string $builder_id): array {
393    $build = [];
394
395    foreach ($this->definitionsFiltered as $component_id => $definition) {
395    foreach ($this->definitionsFiltered as $component_id => $definition) {
395    foreach ($this->definitionsFiltered as $component_id => $definition) {
396      $build[] = [
397        '#type' => 'html_tag',
398        '#tag' => 'h4',
399        '#value' => $definition['annotated_name'],
400        '#attributes' => [
401          'data-filter-parent' => $definition['machineName'],
402        ],
403      ];
404
405      $data = [
406        'source_id' => 'component',
407        'source' => $this->sourcesData[$component_id],
408      ];
409
410      if (!isset($definition['variants'])) {
411        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
412        // Used for search filter.
413        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
414        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords);
415        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
416        // Label is used by default to set drawer title when dragging. It is set
417        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
418        // to override it to have the proper label and not the variant name.
419        // @see assets/js/db_drawer.js
420        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
421        $build_variant['#attributes']['data-node-title'] = $definition['label'];
422
423        $build[] = $build_variant;
424
425        continue;
428      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
428      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
428      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
395    foreach ($this->definitionsFiltered as $component_id => $definition) {
396      $build[] = [
397        '#type' => 'html_tag',
398        '#tag' => 'h4',
399        '#value' => $definition['annotated_name'],
400        '#attributes' => [
401          'data-filter-parent' => $definition['machineName'],
402        ],
403      ];
404
405      $data = [
406        'source_id' => 'component',
407        'source' => $this->sourcesData[$component_id],
408      ];
409
410      if (!isset($definition['variants'])) {
411        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
412        // Used for search filter.
413        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
414        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords);
415        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
416        // Label is used by default to set drawer title when dragging. It is set
417        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
418        // to override it to have the proper label and not the variant name.
419        // @see assets/js/db_drawer.js
420        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
421        $build_variant['#attributes']['data-node-title'] = $definition['label'];
422
423        $build[] = $build_variant;
424
425        continue;
426      }
427
428      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
395    foreach ($this->definitionsFiltered as $component_id => $definition) {
396      $build[] = [
397        '#type' => 'html_tag',
398        '#tag' => 'h4',
399        '#value' => $definition['annotated_name'],
400        '#attributes' => [
401          'data-filter-parent' => $definition['machineName'],
402        ],
403      ];
404
405      $data = [
406        'source_id' => 'component',
407        'source' => $this->sourcesData[$component_id],
408      ];
409
410      if (!isset($definition['variants'])) {
411        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', ['component_id' => $component_id]);
412        // Used for search filter.
413        $keywords = \sprintf('%s %s', $definition['label'], $definition['provider']);
414        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $this->t('Default'), $data, $component_preview_url, $keywords);
415        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
416        // Label is used by default to set drawer title when dragging. It is set
417        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
418        // to override it to have the proper label and not the variant name.
419        // @see assets/js/db_drawer.js
420        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
421        $build_variant['#attributes']['data-node-title'] = $definition['label'];
422
423        $build[] = $build_variant;
424
425        continue;
426      }
427
428      foreach ($definition['variants'] ?? [] as $variant_id => $variant) {
429        $params = ['component_id' => $component_id, 'variant_id' => $variant_id];
430        $component_preview_url = Url::fromRoute('display_builder.api_component_preview', $params);
431        $data['source']['component']['variant_id'] = [
432          'source_id' => 'select',
433          'source' => [
434            'value' => $variant_id,
435          ],
436        ];
437        // Used for search filter.
438        $keywords = \sprintf('%s %s %s', $definition['label'], $variant['title'], $definition['provider']);
439        $build_variant = $this->buildPlaceholderButtonWithPreview($builder_id, $variant['title'], $data, $component_preview_url, $keywords);
440        $build_variant['#attributes']['data-filter-child'] = $definition['machineName'];
441        // Label is used by default to set drawer title when dragging. It is set
442        // on RenderableBuilderTrait::buildPlaceholderButton(), so here we need
443        // to override it to have the proper label and not the variant name.
444        // @see assets/js/db_drawer.js
445        // @see src/RenderableBuilderTrait::buildPlaceholderButton()
446        $build_variant['#attributes']['data-node-title'] = $definition['label'];
447
448        $build[] = $build_variant;
449      }
450    }
451
452    return $this->buildDraggables($builder_id, $build);
ComponentLibraryPanel->getProviders
324  protected function getProviders(array $definitions): array {
325    $themes = $this->themeList->getAllInstalledInfo();
326    $modules = $this->moduleList->getAllInstalledInfo();
327    $providers = [];
328
329    foreach ($definitions as $definition) {
329    foreach ($definitions as $definition) {
330      $provider_id = $definition['provider'];
331
332      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
333
334      if (!$provider) {
335        continue;
337      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
337      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
337      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
329    foreach ($definitions as $definition) {
330      $provider_id = $definition['provider'];
331
332      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
333
334      if (!$provider) {
335        continue;
336      }
337      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
329    foreach ($definitions as $definition) {
330      $provider_id = $definition['provider'];
331
332      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
333
334      if (!$provider) {
335        continue;
336      }
337      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
338      $providers[$provider_id] = $provider;
339    }
340
341    return $providers;
ComponentLibraryPanel->getProvidersOptions
507  private function getProvidersOptions(array $definitions, string|TranslatableMarkup $singular = 'definition', string|TranslatableMarkup $plural = 'definitions'): array {
508    $options = [];
509
510    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
510    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
510    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
510    foreach ($this->getProviders($definitions) as $provider_id => $provider) {
511      $params = [
512        '@name' => $provider['name'],
513        '@type' => $provider['type'],
514        '@count' => $provider['count'],
515        '@singular' => $singular,
516        '@plural' => $plural,
517      ];
518      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@type, @count @singular)', '@name (@type, @count @plural)', $params);
519    }
520
521    return $options;
ComponentLibraryPanel->label
98    return 'Components';
ComponentLibraryPanel->validateConfigurationForm
188  public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
189    $values = $form_state->getValues();
190
191    // At least one display must be enabled.
192    $show_grouped = (bool) $values['show_grouped'];
193    $show_variants = (bool) $values['show_variants'];
194    $show_mosaic = (bool) $values['show_mosaic'];
195
196    if (!$show_grouped && !$show_variants && !$show_mosaic) {
196    if (!$show_grouped && !$show_variants && !$show_mosaic) {
196    if (!$show_grouped && !$show_variants && !$show_mosaic) {
197      $form_state->setError($form['show_grouped'], $this->t('At least one display must be selected!'));
198      $form_state->setError($form['show_variants'], $this->t('At least one display must be selected!'));
199      $form_state->setError($form['show_mosaic'], $this->t('At least one display must be selected!'));
200    }
201  }
201  }