Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiPreviewController
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 10
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 getBlockPreview
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
 getPresetPreview
0.00% covered (danger)
0.00%
0 / 3
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
 getComponentPreview
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 generateBlock
0.00% covered (danger)
0.00%
0 / 7
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
 buildResponse
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getComponentDescription
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 generateStory
0.00% covered (danger)
0.00%
0 / 9
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
 generateComponent
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 1
110
 renderSource
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\Core\Controller\ControllerBase;
8use Drupal\Core\Render\HtmlResponse;
9use Drupal\Core\Render\RendererInterface;
10use Drupal\Core\Theme\ComponentPluginManager;
11use Drupal\display_builder\RenderableBuilderTrait;
12use Drupal\ui_patterns_library\StoryPluginManager;
13use Symfony\Component\DependencyInjection\Attribute\Autowire;
14
15/**
16 * Returns preview responses for Display builder routes.
17 */
18class ApiPreviewController extends ControllerBase {
19
20  use RenderableBuilderTrait;
21
22  public function __construct(
23    #[Autowire(service: 'plugin.manager.component_story')]
24    private StoryPluginManager $storyPluginManager,
25    #[Autowire(service: 'plugin.manager.sdc')]
26    private ComponentPluginManager $componentManager,
27    private RendererInterface $renderer,
28  ) {}
29
30  /**
31   * Get block preview.
32   *
33   * @param string $block_id
34   *   Block ID.
35   *
36   * @return \Drupal\Core\Render\HtmlResponse
37   *   The HTML response.
38   */
39  public function getBlockPreview(string $block_id): HtmlResponse {
40    return $this->buildResponse($this->generateBlock($block_id));
41  }
42
43  /**
44   * Get preset preview.
45   *
46   * @param string $preset_id
47   *   Preset ID.
48   *
49   * @return \Drupal\Core\Render\HtmlResponse
50   *   The HTML response.
51   */
52  public function getPresetPreview(string $preset_id): HtmlResponse {
53    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
54    $preset = $this->entityTypeManager()->getStorage('pattern_preset')->load($preset_id);
55    $data = $preset->getSources([], FALSE);
56
57    return $this->buildResponse($this->renderSource($data), $preset->get('description'));
58  }
59
60  /**
61   * Get component preview, only HTML without specific css or js.
62   *
63   * @param string $component_id
64   *   Component ID.
65   * @param string $variant_id
66   *   Component variant ID.
67   *
68   * @return \Drupal\Core\Render\HtmlResponse
69   *   The HTML response.
70   */
71  public function getComponentPreview(string $component_id, string $variant_id): HtmlResponse {
72    $ui_patterns_library = $this->moduleHandler()->moduleExists('ui_patterns_library');
73
74    $build = [];
75
76    if (!$ui_patterns_library) {
77      $build = $this->generateComponent($component_id);
78    }
79    else {
80      $stories = $this->storyPluginManager->getComponentStories($component_id);
81
82      if (empty($stories)) {
83        $build = $this->generateComponent($component_id);
84      }
85      else {
86        $story = [];
87        $first_story = \reset($stories);
88        $story[$first_story['machineName'] ?? 'default'] = $first_story;
89        $build = $this->generateStory($component_id, $variant_id, $story);
90      }
91    }
92
93    return $this->buildResponse($build, $this->getComponentDescription($component_id));
94  }
95
96  /**
97   * Build renderable block.
98   *
99   * @param string $block_id
100   *   The block id to preview.
101   *
102   * @return array
103   *   A renderable array.
104   */
105  protected function generateBlock(string $block_id): array {
106    $data = [
107      'source_id' => 'block',
108      'source' => [
109        'plugin_id' => $block_id,
110      ],
111    ];
112
113    return $this->renderSource($data);
114  }
115
116  /**
117   * Build the preview response, with an optional description footer.
118   *
119   * The rendered thing and its description are wrapped separately so the
120   * description can stay pinned at the bottom of the popup while the render
121   * above it absorbs the clipping, whenever the popup had to be capped to
122   * the room left on screen.
123   *
124   * @param array $build
125   *   The renderable to preview.
126   * @param string|null $description
127   *   (Optional) Description to show below it, when there is one.
128   *
129   * @return \Drupal\Core\Render\HtmlResponse
130   *   The HTML response.
131   *
132   * @see components/display_builder/css/preview.css
133   */
134  private function buildResponse(array $build, ?string $description = NULL): HtmlResponse {
135    $wrapper = [
136      'content' => [
137        '#type' => 'container',
138        '#attributes' => ['class' => ['db-preview__content']],
139        'content' => $build,
140      ],
141    ];
142
143    if ($description !== NULL && \trim($description) !== '') {
144      $wrapper['description'] = [
145        '#type' => 'container',
146        '#attributes' => ['class' => ['db-preview__description']],
147        'text' => ['#plain_text' => $description],
148      ];
149    }
150
151    $response = new HtmlResponse();
152    $response->setContent($this->renderer->renderRoot($wrapper));
153
154    return $response;
155  }
156
157  /**
158   * Get a component description, if it declares one.
159   *
160   * @param string $component_id
161   *   The component id.
162   *
163   * @return string|null
164   *   The description, or NULL when the component has none or is unknown.
165   */
166  private function getComponentDescription(string $component_id): ?string {
167    try {
168      $definition = $this->componentManager->getDefinition($component_id);
169    }
170    catch (\Throwable $th) {
171      return NULL;
172    }
173
174    $description = $definition['description'] ?? NULL;
175
176    return $description === NULL ? NULL : (string) $description;
177  }
178
179  /**
180   * Generate a story.
181   *
182   * @param string $component_id
183   *   The component id.
184   * @param string $variant_id
185   *   The component variant id.
186   * @param array $stories
187   *   The stories.
188   *
189   * @return array
190   *   The render array
191   */
192  private function generateStory(string $component_id, string $variant_id, array $stories): array {
193    $html = [];
194
195    foreach (\array_keys($stories) as $story_id) {
196      $html[$story_id] = [
197        '#type' => 'component',
198        '#component' => $component_id,
199        '#story' => $story_id,
200        '#props' => ['variant' => $variant_id],
201      ];
202    }
203
204    return $html;
205  }
206
207  /**
208   * Generate a content even with empty story or no library module.
209   *
210   * @param string $component_id
211   *   The component id.
212   *
213   * @return array
214   *   The render array
215   *
216   * @todo Remove when https://www.drupal.org/project/ui_patterns/issues/3414774 is merged
217   */
218  private function generateComponent(string $component_id): array {
219    try {
220      $definition = $this->componentManager->getDefinition($component_id);
221    }
222    catch (\Throwable $th) {
223      return [];
224    }
225
226    $html = [
227      '#type' => 'component',
228      '#component' => $component_id,
229    ];
230
231    foreach ($definition['slots'] ?? [] as $slot_id => $slot) {
232      if (isset($slot['examples']) && \is_array($slot['examples']) && !empty($slot['examples'])) {
233        $html['#slots'][$slot_id] = $slot['examples'][0];
234      }
235    }
236
237    foreach ($definition['props']['properties'] ?? [] as $prop_id => $prop) {
238      if (isset($prop['examples']) && \is_array($prop['examples']) && !empty($prop['examples'])) {
239        $html['#props'][$prop_id] = $prop['examples'][0];
240      }
241    }
242
243    return $html;
244  }
245
246  /**
247   * Get renderable array for a slot source.
248   *
249   * @param array $data
250   *   The slot source data array containing:
251   *   - source_id: The source ID
252   *   - source: Array of source configuration.
253   *
254   * @return array
255   *   The renderable array for this slot source.
256   */
257  private function renderSource(array $data): array {
258    /** @var \Drupal\ui_patterns\Element\ComponentElementBuilder $builder */
259    $builder = \Drupal::service('ui_patterns.component_element_builder'); // @phpcs:ignore
260
261    try {
262      $build = $builder->buildSource([], 'content', [], $data, []) ?? [];
263
264      return $build['#slots']['content'][0] ?? [];
265    }
266    catch (\Throwable $th) {
267      return [];
268    }
269  }
270
271}