Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
90.55% covered (success)
90.55%
115 / 127
81.69% covered (warning)
81.69%
58 / 71
25.42% covered (danger)
25.42%
15 / 59
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockLibrarySourceHelper
90.55% covered (success)
90.55%
115 / 127
81.69% covered (warning)
81.69%
58 / 71
25.42% covered (danger)
25.42%
15 / 59
16.67% covered (danger)
16.67%
1 / 6
669.86
0.00% covered (danger)
0.00%
0 / 1
 getGroupedChoices
93.33% covered (success)
93.33%
14 / 15
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
4.00
 sortGroupedChoices
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getChoices
92.86% covered (success)
92.86%
39 / 42
80.00% covered (warning)
80.00%
16 / 20
18.18% covered (danger)
18.18%
2 / 11
0.00% covered (danger)
0.00%
0 / 1
53.36
 isChoiceValid
88.89% covered (warning)
88.89%
8 / 9
88.89% covered (warning)
88.89%
8 / 9
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
19.85
 getSourceGroupLabel
79.17% covered (warning)
79.17%
19 / 24
78.95% covered (warning)
78.95%
15 / 19
26.32% covered (danger)
26.32%
5 / 19
0.00% covered (danger)
0.00%
0 / 1
59.41
 getChoiceGroupLabel
87.50% covered (warning)
87.50%
14 / 16
78.57% covered (warning)
78.57%
11 / 14
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
19.63
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder;
6
7use Drupal\Component\Render\MarkupInterface;
8use Drupal\Core\StringTranslation\TranslatableMarkup;
9use Drupal\Core\Url;
10
11/**
12 * Helper class for block library source handling.
13 */
14class BlockLibrarySourceHelper {
15
16  private const HIDE_BLOCK = [
17    'htmx_loader',
18    'broken',
19    'system_main_block',
20    'page_title_block',
21  ];
22
23  private const HIDE_CHOICE_ID = [
24    'field:user:user:pass',
25  ];
26
27  /**
28   * Get the choices grouped by category.
29   *
30   * @param array $sources
31   *   An array of all possible sources.
32   * @param array $exclude_provider
33   *   (Optional) An array of providers to hide.
34   * @param array $exclude_by_id
35   *   (Optional) An array of id to hide.
36   * @param bool $preview
37   *   (Default true) Whether to show preview on hover.
38   *
39   * @return array
40   *   An array of grouped choices.
41   */
42  public static function getGroupedChoices(array $sources, array $exclude_provider = [], array $exclude_by_id = [], bool $preview = TRUE): array {
43    $choices = self::getChoices($sources, $exclude_provider, $exclude_by_id, $preview);
44    $default_category = (string) new TranslatableMarkup('Others');
45
46    $categories = [];
47
48    foreach ($choices as $choice) {
49      $category = $choice['group'] ?? $default_category;
50
51      if ($category instanceof MarkupInterface) {
52        $category = (string) $category;
53      }
54
55      if (!isset($categories[$category])) {
56        $categories[$category] = [
57          'label' => $category,
58          'choices' => [],
59        ];
60      }
61      $categories[$category]['choices'][] = $choice;
62    }
63    self::sortGroupedChoices($categories);
64
65    return $categories;
66  }
67
68  /**
69   * Sorts the grouped choices based on arbitrary weight.
70   *
71   * @param array $categories
72   *   The categories to sort, passed by reference.
73   */
74  public static function sortGroupedChoices(array &$categories): void {
75    // Public for reuse by PresetLibraryPanel.
76    $category_weight = [
77      // Different builder contexts.
78      (string) new TranslatableMarkup('Page') => 1,
79      (string) new TranslatableMarkup('Views') => 1,
80      (string) new TranslatableMarkup('Fields') => 1,
81      (string) new TranslatableMarkup('Referenced entities') => 2,
82      // Global categories.
83      (string) new TranslatableMarkup('Menus') => 3,
84      (string) new TranslatableMarkup('System') => 3,
85      (string) new TranslatableMarkup('User') => 3,
86      (string) new TranslatableMarkup('Utilities') => 3,
87      (string) new TranslatableMarkup('Forms') => 4,
88      (string) new TranslatableMarkup('Lists (Views)') => 5,
89      (string) new TranslatableMarkup('Others') => 6,
90      (string) new TranslatableMarkup('Dev tools') => 99,
91    ];
92
93    // Sort categories by predefined weight, then by natural string comparison
94    // of labels.
95    \uasort($categories, static function ($a, $b) use ($category_weight) {
96      $weight_a = $category_weight[$a['label']] ?? 98;
97      $weight_b = $category_weight[$b['label']] ?? 98;
98
99      if ($weight_a === $weight_b) {
100        return \strnatcmp($a['label'], $b['label']);
101      }
102
103      return $weight_a <=> $weight_b;
104    });
105  }
106
107  /**
108   * Get the choices from all sources.
109   *
110   * @param array $sources
111   *   An array of all possible sources.
112   * @param array $exclude_provider
113   *   An array of providers to hide.
114   * @param array $exclude_by_id
115   *   (Optional) An array of id to hide.
116   * @param bool $preview
117   *   (Default true) Whether to show preview on hover.
118   *
119   * @return array
120   *   An array of choices.
121   */
122  public static function getChoices(array $sources, array $exclude_provider, array $exclude_by_id = [], bool $preview = TRUE): array {
123    $result_choices = [];
124
125    foreach ($sources as $source_id => $source_data) {
126      $definition = $source_data['definition'];
127      $source = $source_data['source'];
128
129      // If no choices, add the source as a single choice.
130      if (!isset($source_data['choices'])) {
131        $id = $definition['id'] ?? $source_id;
132
133        if (\in_array($id, $exclude_by_id, TRUE)) {
134          continue;
135        }
136
137        $label = $definition['label'] ?? $source_id;
138        $keywords = \sprintf('%s %s %s', $definition['id'], $definition['label'] ?? $source_id, $definition['description'] ?? '');
139
140        $result_choices[] = [
141          'label' => (string) $label,
142          'data' => ['source_id' => $source_id],
143          'keywords' => $keywords,
144          'preview' => NULL,
145          'group' => self::getSourceGroupLabel($definition),
146        ];
147
148        continue;
149      }
150
151      // Mostly for block source, iterate over choices.
152      $choices = $source_data['choices'];
153
154      foreach ($choices as $choice_id => $choice) {
155        if (\in_array($choice_id, $exclude_by_id, TRUE)) {
156          continue;
157        }
158
159        if (\in_array($choice_id, self::HIDE_CHOICE_ID, TRUE)) {
160          continue;
161        }
162
163        if (!self::isChoiceValid($choice, $definition, $exclude_provider)) {
164          continue;
165        }
166
167        $label = $choice['label'] ?? $choice_id;
168        $keywords = \sprintf('%s %s %s %s', $definition['id'], $label, $definition['description'] ?? '', $choice_id);
169
170        $result_choices[] = [
171          'label' => (string) $label,
172          'keywords' => $keywords,
173          'data' => [
174            'source_id' => $source_id,
175            'source' => $source->getChoiceSettings($choice_id),
176          ],
177          'preview' => $preview ? Url::fromRoute('display_builder.api_block_preview', ['block_id' => $choice_id]) : NULL,
178          'group' => self::getChoiceGroupLabel($choice, $definition),
179        ];
180      }
181    }
182
183    // Sort alphabetically within groups by label.
184    \usort($result_choices, static function (array $a, array $b): int {
185      return \strnatcasecmp((string) ($a['label']), (string) ($b['label']));
186    });
187
188    return $result_choices;
189  }
190
191  /**
192   * Validate a choice against the source definition and allowed providers.
193   *
194   * @param array $choice
195   *   The choice to validate.
196   * @param array $source_definition
197   *   The source definition.
198   * @param array $exclude_provider
199   *   The excluded providers.
200   *
201   * @return bool
202   *   Whether the choice is valid or not.
203   */
204  private static function isChoiceValid(array &$choice, array &$source_definition, array $exclude_provider = []): bool {
205    $provider = $choice['provider'] ?? '';
206
207    if ($provider) {
208      if (\in_array($provider, $exclude_provider, TRUE)) {
209        return FALSE;
210      }
211    }
212
213    if ($source_definition['id'] === 'block') {
214      $block_id = $choice['original_id'] ?? '';
215
216      if ($block_id && \in_array($block_id, self::HIDE_BLOCK, TRUE)) {
217        return FALSE;
218      }
219    }
220
221    return TRUE;
222  }
223
224  /**
225   * Get the group label for a source.
226   *
227   * @param array $source_definition
228   *   The source definition to use for the group.
229   *
230   * @return string
231   *   The group label for the choice.
232   */
233  private static function getSourceGroupLabel(array $source_definition): string {
234    $group = (string) new TranslatableMarkup('Others');
235
236    $provider = $source_definition['provider'] ?? NULL;
237
238    if (!$provider) {
239      return $group;
240    }
241
242    switch ($provider) {
243      case 'display_builder_page_layout':
244        $group = (string) new TranslatableMarkup('Page');
245
246        break;
247
248      case 'display_builder_views':
249      case 'ui_patterns_views':
250        $group = (string) new TranslatableMarkup('Views');
251
252        break;
253
254      case 'display_builder_entity_view':
255        $group = (string) new TranslatableMarkup('Fields');
256
257        break;
258
259      case 'display_builder_dev_tools':
260        $group = (string) new TranslatableMarkup('Dev tools');
261
262        break;
263
264      case 'display_builder':
265      case 'ui_icons_patterns':
266      case 'ui_patterns':
267        $group = (string) new TranslatableMarkup('Utilities');
268
269        break;
270
271      default:
272        break;
273    }
274
275    return $group;
276  }
277
278  /**
279   * Get the group label for a choice.
280   *
281   * @param array $choice
282   *   The choice to get the group for.
283   * @param array $source_definition
284   *   The source definition to use for the group.
285   *
286   * @return string
287   *   The group label for the choice.
288   */
289  private static function getChoiceGroupLabel(array $choice, array $source_definition): string {
290    $group = (string) new TranslatableMarkup('Others');
291    $source_id = $source_definition['id'] ?? NULL;
292
293    if (!$source_id) {
294      return $group;
295    }
296
297    switch ($source_id) {
298      case 'block':
299        if (!empty($choice['group'])) {
300          $group = $choice['group'];
301        }
302
303        break;
304
305      case 'entity_reference':
306        $group = (string) new TranslatableMarkup('Referenced entities');
307
308        break;
309
310      case 'entity_field':
311        $group = (string) new TranslatableMarkup('Fields');
312
313        break;
314
315      default:
316        break;
317    }
318
319    return $group;
320  }
321
322}